18 luar "layeh.com/gopher-luar"
20 "github.com/zyedidia/micro/internal/config"
21 ulua "github.com/zyedidia/micro/internal/lua"
22 "github.com/zyedidia/micro/internal/screen"
23 . "github.com/zyedidia/micro/internal/util"
24 "github.com/zyedidia/micro/pkg/highlight"
25 "golang.org/x/text/encoding/htmlindex"
26 "golang.org/x/text/encoding/unicode"
27 "golang.org/x/text/transform"
30 const backup_time = 8000
37 // The BufType defines what kind of buffer this is
40 Readonly bool // The buffer cannot be edited
41 Scratch bool // The buffer cannot be saved
42 Syntax bool // Syntax highlighting is enabled
46 BTDefault = BufType{0, false, false, true}
47 BTHelp = BufType{1, true, true, true}
48 BTLog = BufType{2, true, true, false}
49 BTScratch = BufType{3, false, true, false}
50 BTRaw = BufType{4, false, true, false}
51 BTInfo = BufType{5, false, true, false}
53 ErrFileTooLarge = errors.New("File is too large to hash")
56 type SharedBuffer struct {
58 // Stores the last modification time of the file the buffer is pointing to
60 // Type of the buffer (e.g. help, raw, scratch etc..)
64 // Whether or not suggestions can be autocompleted must be shared because
65 // it changes based on how the buffer has changed
69 func (b *SharedBuffer) insert(pos Loc, value []byte) {
71 b.HasSuggestions = false
72 b.LineArray.insert(pos, value)
74 func (b *SharedBuffer) remove(start, end Loc) []byte {
76 b.HasSuggestions = false
77 return b.LineArray.remove(start, end)
80 // Buffer stores the main information about a currently open file including
81 // the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
82 // all the cursors, the syntax highlighting info, the settings for the buffer
83 // and some misc info about modification time and path location.
84 // The syntax highlighting info must be stored with the buffer because the syntax
85 // highlighter attaches information to each line of the buffer for optimization
86 // purposes so it doesn't have to rehighlight everything on every update.
95 // Path to the file on disk
97 // Absolute path to the file on disk
99 // Name of the buffer on the status line
102 SyntaxDef *highlight.Def
103 Highlighter *highlight.Highlighter
105 // Hash of the original buffer -- empty if fastdirty is on
106 origHash [md5.Size]byte
108 // Settings customized by the user
109 Settings map[string]interface{}
117 // counts the number of edits
118 // resets every backup_time edits
122 // NewBufferFromFile opens a new buffer using the given path
123 // It will also automatically handle `~`, and line/column with filename:l:c
124 // It will return an empty buffer if the path does not exist
125 // and an error if the file is a directory
126 func NewBufferFromFile(path string, btype BufType) (*Buffer, error) {
128 filename, cursorPos := GetPathAndCursorPosition(path)
129 filename, err = ReplaceHome(filename)
134 file, err := os.Open(filename)
135 fileInfo, _ := os.Stat(filename)
137 if err == nil && fileInfo.IsDir() {
138 return nil, errors.New(filename + " is a directory")
143 cursorLoc, cursorerr := ParseCursorLocation(cursorPos)
144 if cursorerr != nil {
145 cursorLoc = Loc{-1, -1}
150 // File does not exist -- create an empty buffer with that name
151 buf = NewBufferFromString("", filename, btype)
153 buf = NewBuffer(file, FSize(file), filename, cursorLoc, btype)
159 // NewBufferFromString creates a new buffer containing the given string
160 func NewBufferFromString(text, path string, btype BufType) *Buffer {
161 return NewBuffer(strings.NewReader(text), int64(len(text)), path, Loc{-1, -1}, btype)
164 // NewBuffer creates a new buffer from a given reader with a given path
165 // Ensure that ReadSettings and InitGlobalSettings have been called before creating
167 // Places the cursor at startcursor. If startcursor is -1, -1 places the
168 // cursor at an autodetected location (based on savecursor or :LINE:COL)
169 func NewBuffer(r io.Reader, size int64, path string, startcursor Loc, btype BufType) *Buffer {
170 absPath, _ := filepath.Abs(path)
174 b.Settings = config.DefaultCommonSettings()
175 for k, v := range config.GlobalSettings {
176 if _, ok := b.Settings[k]; ok {
180 config.InitLocalSettings(b.Settings, path)
182 enc, err := htmlindex.Get(b.Settings["encoding"].(string))
185 b.Settings["encoding"] = "utf-8"
188 reader := transform.NewReader(r, enc.NewDecoder())
192 for _, buf := range OpenBuffers {
193 if buf.AbsPath == absPath && buf.Type != BTInfo {
195 b.SharedBuffer = buf.SharedBuffer
196 b.EventHandler = buf.EventHandler
202 choice := 1 // ignore by default
204 b.SharedBuffer = new(SharedBuffer)
207 if b.Settings["backup"].(bool) {
208 backupfile := config.ConfigDir + "/backups/" + EscapePath(absPath)
209 if info, err := os.Stat(backupfile); err == nil {
210 backup, err := os.Open(backupfile)
213 msg := fmt.Sprintf(backupMsg, t.Format("Mon Jan _2 15:04 2006"))
214 choice = screen.TermPrompt(msg, []string{"r", "i", "recover", "ignore"}, true)
215 log.Println("Choice:", choice)
219 b.LineArray = NewLineArray(uint64(size), FFAuto, backup)
221 } else if choice%2 == 1 {
223 os.Remove(backupfile)
231 b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
233 b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
236 if b.Settings["readonly"].(bool) {
237 b.Type.Readonly = true
243 // The last time this file was modified
244 b.ModTime, _ = GetModTime(b.Path)
248 b.Settings["fileformat"] = "unix"
250 b.Settings["fileformat"] = "dos"
254 config.InitLocalSettings(b.Settings, b.Path)
256 if _, err := os.Stat(config.ConfigDir + "/buffers/"); os.IsNotExist(err) {
257 os.Mkdir(config.ConfigDir+"/buffers/", os.ModePerm)
260 if startcursor.X != -1 && startcursor.Y != -1 {
261 b.StartCursor = startcursor
263 if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
264 err := b.Unserialize()
266 screen.TermMessage(err)
271 b.AddCursor(NewCursor(b, b.StartCursor))
272 b.GetActiveCursor().Relocate()
274 if !b.Settings["fastdirty"].(bool) {
275 if size > LargeFileThreshold {
276 // If the file is larger than LargeFileThreshold fastdirty needs to be on
277 b.Settings["fastdirty"] = true
279 calcHash(b, &b.origHash)
283 err = config.RunPluginFn("onBufferOpen", luar.New(ulua.L, b))
285 screen.TermMessage(err)
288 OpenBuffers = append(OpenBuffers, b)
293 // Close removes this buffer from the list of open buffers
294 func (b *Buffer) Close() {
295 for i, buf := range OpenBuffers {
298 copy(OpenBuffers[i:], OpenBuffers[i+1:])
299 OpenBuffers[len(OpenBuffers)-1] = nil
300 OpenBuffers = OpenBuffers[:len(OpenBuffers)-1]
306 // Fini should be called when a buffer is closed and performs
308 func (b *Buffer) Fini() {
315 // GetName returns the name that should be displayed in the statusline
317 func (b *Buffer) GetName() string {
327 //SetName changes the name for this buffer
328 func (b *Buffer) SetName(s string) {
332 func (b *Buffer) Insert(start Loc, text string) {
333 if !b.Type.Readonly {
334 b.EventHandler.cursors = b.cursors
335 b.EventHandler.active = b.curCursor
336 b.EventHandler.Insert(start, text)
342 func (b *Buffer) Remove(start, end Loc) {
343 if !b.Type.Readonly {
344 b.EventHandler.cursors = b.cursors
345 b.EventHandler.active = b.curCursor
346 b.EventHandler.Remove(start, end)
352 // FileType returns the buffer's filetype
353 func (b *Buffer) FileType() string {
354 return b.Settings["filetype"].(string)
357 // ReOpen reloads the current buffer from disk
358 func (b *Buffer) ReOpen() error {
359 file, err := os.Open(b.Path)
364 enc, err := htmlindex.Get(b.Settings["encoding"].(string))
369 reader := transform.NewReader(file, enc.NewDecoder())
370 data, err := ioutil.ReadAll(reader)
376 b.EventHandler.ApplyDiff(txt)
378 b.ModTime, err = GetModTime(b.Path)
384 func (b *Buffer) RelocateCursors() {
385 for _, c := range b.cursors {
390 // RuneAt returns the rune at a given location in the buffer
391 func (b *Buffer) RuneAt(loc Loc) rune {
392 line := b.LineBytes(loc.Y)
396 r, size := utf8.DecodeRune(line)
408 // Modified returns if this buffer has been modified since
410 func (b *Buffer) Modified() bool {
415 if b.Settings["fastdirty"].(bool) {
419 var buff [md5.Size]byte
422 return buff != b.origHash
425 // calcHash calculates md5 hash of all lines in the buffer
426 func calcHash(b *Buffer, out *[md5.Size]byte) error {
430 if len(b.lines) > 0 {
431 n, e := h.Write(b.lines[0].data)
437 for _, l := range b.lines[1:] {
438 n, e = h.Write([]byte{'\n'})
443 n, e = h.Write(l.data)
451 if size > LargeFileThreshold {
452 return ErrFileTooLarge
459 // UpdateRules updates the syntax rules and filetype for this buffer
460 // This is called when the colorscheme changes
461 func (b *Buffer) UpdateRules() {
466 var files []*highlight.File
467 for _, f := range config.ListRuntimeFiles(config.RTSyntax) {
468 data, err := f.Data()
470 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
472 file, err := highlight.ParseFile(data)
474 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
477 ftdetect, err := highlight.ParseFtDetect(file)
479 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
483 ft := b.Settings["filetype"].(string)
484 if (ft == "unknown" || ft == "") && !rehighlight {
485 if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
486 header := new(highlight.Header)
487 header.FileType = file.FileType
488 header.FtDetect = ftdetect
489 b.SyntaxDef, err = highlight.ParseDef(file, header)
491 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
497 if file.FileType == ft && !rehighlight {
498 header := new(highlight.Header)
499 header.FileType = file.FileType
500 header.FtDetect = ftdetect
501 b.SyntaxDef, err = highlight.ParseDef(file, header)
503 screen.TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
509 files = append(files, file)
513 if b.SyntaxDef != nil {
514 highlight.ResolveIncludes(b.SyntaxDef, files)
517 if b.Highlighter == nil || rehighlight {
518 if b.SyntaxDef != nil {
519 b.Settings["filetype"] = b.SyntaxDef.FileType
520 b.Highlighter = highlight.NewHighlighter(b.SyntaxDef)
521 if b.Settings["syntax"].(bool) {
522 b.Highlighter.HighlightStates(b)
528 // ClearMatches clears all of the syntax highlighting for the buffer
529 func (b *Buffer) ClearMatches() {
530 for i := range b.lines {
536 // IndentString returns this buffer's indent method (a tabstop or n spaces
537 // depending on the settings)
538 func (b *Buffer) IndentString(tabsize int) string {
539 if b.Settings["tabstospaces"].(bool) {
540 return Spaces(tabsize)
545 // SetCursors resets this buffer's cursors to a new list
546 func (b *Buffer) SetCursors(c []*Cursor) {
548 b.EventHandler.cursors = b.cursors
549 b.EventHandler.active = b.curCursor
552 // AddCursor adds a new cursor to the list
553 func (b *Buffer) AddCursor(c *Cursor) {
554 b.cursors = append(b.cursors, c)
555 b.EventHandler.cursors = b.cursors
556 b.EventHandler.active = b.curCursor
560 // SetCurCursor sets the current cursor
561 func (b *Buffer) SetCurCursor(n int) {
565 // GetActiveCursor returns the main cursor in this buffer
566 func (b *Buffer) GetActiveCursor() *Cursor {
567 return b.cursors[b.curCursor]
570 // GetCursor returns the nth cursor
571 func (b *Buffer) GetCursor(n int) *Cursor {
575 // GetCursors returns the list of cursors in this buffer
576 func (b *Buffer) GetCursors() []*Cursor {
580 // NumCursors returns the number of cursors
581 func (b *Buffer) NumCursors() int {
582 return len(b.cursors)
585 // MergeCursors merges any cursors that are at the same position
587 func (b *Buffer) MergeCursors() {
588 var cursors []*Cursor
589 for i := 0; i < len(b.cursors); i++ {
592 for j := 0; j < len(b.cursors); j++ {
594 if c2 != nil && i != j && c1.Loc == c2.Loc {
598 cursors = append(cursors, c1)
604 for i := range b.cursors {
608 if b.curCursor >= len(b.cursors) {
609 b.curCursor = len(b.cursors) - 1
611 b.EventHandler.cursors = b.cursors
612 b.EventHandler.active = b.curCursor
615 // UpdateCursors updates all the cursors indicies
616 func (b *Buffer) UpdateCursors() {
617 b.EventHandler.cursors = b.cursors
618 b.EventHandler.active = b.curCursor
619 for i, c := range b.cursors {
624 func (b *Buffer) RemoveCursor(i int) {
625 copy(b.cursors[i:], b.cursors[i+1:])
626 b.cursors[len(b.cursors)-1] = nil
627 b.cursors = b.cursors[:len(b.cursors)-1]
628 b.curCursor = Clamp(b.curCursor, 0, len(b.cursors)-1)
632 // ClearCursors removes all extra cursors
633 func (b *Buffer) ClearCursors() {
634 for i := 1; i < len(b.cursors); i++ {
637 b.cursors = b.cursors[:1]
640 b.GetActiveCursor().ResetSelection()
643 // MoveLinesUp moves the range of lines up one row
644 func (b *Buffer) MoveLinesUp(start int, end int) {
645 if start < 1 || start >= end || end > len(b.lines) {
648 l := string(b.LineBytes(start - 1))
649 if end == len(b.lines) {
652 utf8.RuneCount(b.lines[end-1].data),
669 // MoveLinesDown moves the range of lines down one row
670 func (b *Buffer) MoveLinesDown(start int, end int) {
671 if start < 0 || start >= end || end >= len(b.lines)-1 {
674 l := string(b.LineBytes(end))
686 var BracePairs = [][2]rune{
692 // FindMatchingBrace returns the location in the buffer of the matching bracket
693 // It is given a brace type containing the open and closing character, (for example
694 // '{' and '}') as well as the location to match from
695 // TODO: maybe can be more efficient with utf8 package
696 // returns the location of the matching brace
697 // if the boolean returned is true then the original matching brace is one character left
698 // of the starting location
699 func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) (Loc, bool) {
700 curLine := []rune(string(b.LineBytes(start.Y)))
702 if start.X >= 0 && start.X < len(curLine) {
703 startChar = curLine[start.X]
706 if start.X-1 >= 0 && start.X-1 < len(curLine) {
707 leftChar = curLine[start.X-1]
710 if startChar == braceType[0] || leftChar == braceType[0] {
711 for y := start.Y; y < b.LinesNum(); y++ {
712 l := []rune(string(b.LineBytes(y)))
715 if startChar == braceType[0] {
721 for x := xInit; x < len(l); x++ {
723 if r == braceType[0] {
725 } else if r == braceType[1] {
728 if startChar == braceType[0] {
729 return Loc{x, y}, false
731 return Loc{x, y}, true
736 } else if startChar == braceType[1] || leftChar == braceType[1] {
737 for y := start.Y; y >= 0; y-- {
738 l := []rune(string(b.lines[y].data))
741 if leftChar == braceType[1] {
747 for x := xInit; x >= 0; x-- {
749 if r == braceType[0] {
752 if leftChar == braceType[1] {
753 return Loc{x, y}, true
755 return Loc{x, y}, false
757 } else if r == braceType[1] {
766 // Retab changes all tabs to spaces or vice versa
767 func (b *Buffer) Retab() {
768 toSpaces := b.Settings["tabstospaces"].(bool)
769 tabsize := IntOpt(b.Settings["tabsize"])
772 for i := 0; i < b.LinesNum(); i++ {
775 ws := GetLeadingWhitespace(l)
778 ws = bytes.Replace(ws, []byte{'\t'}, bytes.Repeat([]byte{' '}, tabsize), -1)
780 ws = bytes.Replace(ws, bytes.Repeat([]byte{' '}, tabsize), []byte{'\t'}, -1)
784 l = bytes.TrimLeft(l, " \t")
785 b.lines[i].data = append(ws, l...)
792 // ParseCursorLocation turns a cursor location like 10:5 (LINE:COL)
794 func ParseCursorLocation(cursorPositions []string) (Loc, error) {
795 startpos := Loc{0, 0}
798 // if no positions are available exit early
799 if cursorPositions == nil {
800 return startpos, errors.New("No cursor positions were provided.")
803 startpos.Y, err = strconv.Atoi(cursorPositions[0])
806 if len(cursorPositions) > 1 {
807 startpos.X, err = strconv.Atoi(cursorPositions[1])
817 func (b *Buffer) Line(i int) string {
818 return string(b.LineBytes(i))
821 func WriteLog(s string) {
822 LogBuf.EventHandler.Insert(LogBuf.End(), s)