21 "github.com/zyedidia/micro/cmd/micro/highlight"
24 const LargeFileThreshold = 50000
27 // 0 - no line type detected
33 // Buffer stores the text for files that are loaded into the text editor
34 // It uses a rope to efficiently store the string and contains some
35 // simple functions for saving and wrapper functions for modifying the rope
37 // The eventhandler for undo/redo
39 // This stores all the text in the buffer as an array of lines
43 cursors []*Cursor // for multiple cursors
44 curCursor int // the current cursor
46 // Path to the file on disk
48 // Absolute path to the file on disk
50 // Name of the buffer on the status line
53 // Whether or not the buffer has been modified since it was opened
56 // Stores the last modification time of the file the buffer is pointing to
59 // NumLines is the number of lines in the buffer
62 syntaxDef *highlight.Def
63 highlighter *highlight.Highlighter
65 // Hash of the original buffer -- empty if fastdirty is on
66 origHash [md5.Size]byte
68 // Buffer local settings
69 Settings map[string]interface{}
72 // The SerializedBuffer holds the types that get serialized when a buffer is saved
73 // These are used for the savecursor and saveundo options
74 type SerializedBuffer struct {
75 EventHandler *EventHandler
80 // NewBufferFromFile opens a new buffer using the given path
81 // It will also automatically handle `~`, and line/column with filename:l:c
82 // It will return an empty buffer if the path does not exist
83 // and an error if the file is a directory
84 func NewBufferFromFile(path string) (*Buffer, error) {
85 filename, cursorPosition := GetPathAndCursorPosition(path)
86 filename = ReplaceHome(filename)
87 file, err := os.Open(filename)
88 fileInfo, _ := os.Stat(filename)
90 if err == nil && fileInfo.IsDir() {
91 return nil, errors.New(filename + " is a directory")
98 // File does not exist -- create an empty buffer with that name
99 buf = NewBufferFromString("", filename)
101 buf = NewBuffer(file, FSize(file), filename, cursorPosition)
107 // NewBufferFromString creates a new buffer containing the given string
108 func NewBufferFromString(text, path string) *Buffer {
109 return NewBuffer(strings.NewReader(text), int64(len(text)), path, nil)
112 // NewBuffer creates a new buffer from a given reader with a given path
113 func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []string) *Buffer {
114 // check if the file is already open in a tab. If it's open return the buffer to that tab
116 for _, tab := range tabs {
117 for _, view := range tab.Views {
118 if view.Buf.Path == path {
126 b.LineArray = NewLineArray(size, reader)
128 b.Settings = DefaultLocalSettings()
129 for k, v := range globalSettings {
130 if _, ok := b.Settings[k]; ok {
136 b.Settings["fileformat"] = "unix"
137 } else if fileformat == 2 {
138 b.Settings["fileformat"] = "dos"
141 absPath, _ := filepath.Abs(path)
146 // The last time this file was modified
147 b.ModTime, _ = GetModTime(b.Path)
149 b.EventHandler = NewEventHandler(b)
154 if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
155 os.Mkdir(configDir+"/buffers/", os.ModePerm)
158 cursorLocation, cursorLocationError := GetBufferCursorLocation(cursorPosition, b)
166 if cursorLocationError != nil && len(*flagStartPos) == 0 && (b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool)) {
167 // If either savecursor or saveundo is turned on, we need to load the serialized information
168 // from ~/.config/micro/buffers
169 file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath))
172 var buffer SerializedBuffer
173 decoder := gob.NewDecoder(file)
174 gob.Register(TextEvent{})
175 err = decoder.Decode(&buffer)
177 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.")
179 if b.Settings["savecursor"].(bool) {
180 b.Cursor = buffer.Cursor
185 if b.Settings["saveundo"].(bool) {
186 // We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
187 if b.ModTime == buffer.ModTime {
188 b.EventHandler = buffer.EventHandler
189 b.EventHandler.buf = b
195 if !b.Settings["fastdirty"].(bool) {
196 if size > LargeFileThreshold {
197 // If the file is larger than a megabyte fastdirty needs to be on
198 b.Settings["fastdirty"] = true
200 calcHash(b, &b.origHash)
204 b.cursors = []*Cursor{&b.Cursor}
209 func GetBufferCursorLocation(cursorPosition []string, b *Buffer) (Loc, error) {
210 // parse the cursor position. The cursor location is ALWAYS initialised to 0, 0 even when
211 // an error occurs due to lack of arguments or because the arguments are not numbers
212 cursorLocation, cursorLocationError := ParseCursorLocation(cursorPosition)
214 // Put the cursor at the first spot. In the logic for cursor position the -startpos
215 // flag is processed first and will overwrite any line/col parameters with colons after the filename
216 if len(*flagStartPos) > 0 || cursorLocationError == nil {
217 var lineNum, colNum int
218 var errPos1, errPos2 error
220 positions := strings.Split(*flagStartPos, ",")
222 // if the -startpos flag contains enough args use them for the cursor location.
223 // In this case args passed at the end of the filename will be ignored
224 if len(positions) == 2 {
225 lineNum, errPos1 = strconv.Atoi(positions[0])
226 colNum, errPos2 = strconv.Atoi(positions[1])
228 // if -startpos has invalid arguments, use the arguments from filename.
229 // This will have a default value (0, 0) even when the filename arguments are invalid
230 if errPos1 != nil || errPos2 != nil || len(*flagStartPos) == 0 {
231 // otherwise check if there are any arguments after the filename and use them
232 lineNum = cursorLocation.Y
233 colNum = cursorLocation.X
236 // if some arguments were found make sure they don't go outside the file and cause overflows
237 cursorLocation.Y = lineNum - 1
238 cursorLocation.X = colNum
239 // Check to avoid line overflow
240 if cursorLocation.Y > b.NumLines-1 {
241 cursorLocation.Y = b.NumLines - 1
242 } else if cursorLocation.Y < 0 {
245 // Check to avoid column overflow
246 if cursorLocation.X > len(b.Line(cursorLocation.Y)) {
247 cursorLocation.X = len(b.Line(cursorLocation.Y))
248 } else if cursorLocation.X < 0 {
252 return cursorLocation, cursorLocationError
255 // GetName returns the name that should be displayed in the statusline
257 func (b *Buffer) GetName() string {
267 // UpdateRules updates the syntax rules and filetype for this buffer
268 // This is called when the colorscheme changes
269 func (b *Buffer) UpdateRules() {
271 var files []*highlight.File
272 for _, f := range ListRuntimeFiles(RTSyntax) {
273 data, err := f.Data()
275 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
277 file, err := highlight.ParseFile(data)
279 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
282 ftdetect, err := highlight.ParseFtDetect(file)
284 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
288 ft := b.Settings["filetype"].(string)
289 if (ft == "Unknown" || ft == "") && !rehighlight {
290 if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
291 header := new(highlight.Header)
292 header.FileType = file.FileType
293 header.FtDetect = ftdetect
294 b.syntaxDef, err = highlight.ParseDef(file, header)
296 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
302 if file.FileType == ft && !rehighlight {
303 header := new(highlight.Header)
304 header.FileType = file.FileType
305 header.FtDetect = ftdetect
306 b.syntaxDef, err = highlight.ParseDef(file, header)
308 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
314 files = append(files, file)
318 if b.syntaxDef != nil {
319 highlight.ResolveIncludes(b.syntaxDef, files)
322 if b.highlighter == nil || rehighlight {
323 if b.syntaxDef != nil {
324 b.Settings["filetype"] = b.syntaxDef.FileType
325 b.highlighter = highlight.NewHighlighter(b.syntaxDef)
326 if b.Settings["syntax"].(bool) {
327 b.highlighter.HighlightStates(b)
333 // FileType returns the buffer's filetype
334 func (b *Buffer) FileType() string {
335 return b.Settings["filetype"].(string)
338 // IndentString returns a string representing one level of indentation
339 func (b *Buffer) IndentString() string {
340 if b.Settings["tabstospaces"].(bool) {
341 return Spaces(int(b.Settings["tabsize"].(float64)))
346 // CheckModTime makes sure that the file this buffer points to hasn't been updated
347 // by an external program since it was last read
348 // If it has, we ask the user if they would like to reload the file
349 func (b *Buffer) CheckModTime() {
350 modTime, ok := GetModTime(b.Path)
352 if modTime != b.ModTime {
353 choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
356 if !choice || canceled {
357 // Don't load new changes -- do nothing
358 b.ModTime, _ = GetModTime(b.Path)
367 // ReOpen reloads the current buffer from disk
368 func (b *Buffer) ReOpen() {
369 data, err := ioutil.ReadFile(b.Path)
373 messenger.Error(err.Error())
376 b.EventHandler.ApplyDiff(txt)
378 b.ModTime, _ = GetModTime(b.Path)
384 // Update fetches the string from the rope and updates the `text` and `lines` in the buffer
385 func (b *Buffer) Update() {
386 b.NumLines = len(b.lines)
389 // MergeCursors merges any cursors that are at the same position
391 func (b *Buffer) MergeCursors() {
392 var cursors []*Cursor
393 for i := 0; i < len(b.cursors); i++ {
396 for j := 0; j < len(b.cursors); j++ {
398 if c2 != nil && i != j && c1.Loc == c2.Loc {
402 cursors = append(cursors, c1)
408 for i := range b.cursors {
412 if b.curCursor >= len(b.cursors) {
413 b.curCursor = len(b.cursors) - 1
417 // UpdateCursors updates all the cursors indicies
418 func (b *Buffer) UpdateCursors() {
419 for i, c := range b.cursors {
424 // Save saves the buffer to its default path
425 func (b *Buffer) Save() error {
426 return b.SaveAs(b.Path)
429 // SaveWithSudo saves the buffer to the default path with sudo
430 func (b *Buffer) SaveWithSudo() error {
431 return b.SaveAsWithSudo(b.Path)
434 // Serialize serializes the buffer to configDir/buffers
435 func (b *Buffer) Serialize() error {
436 if !b.Settings["savecursor"].(bool) && !b.Settings["saveundo"].(bool) {
440 name := configDir + "/buffers/" + EscapePath(b.AbsPath)
442 return overwriteFile(name, func(file io.Writer) error {
443 return gob.NewEncoder(file).Encode(SerializedBuffer{
452 gob.Register(TextEvent{})
453 gob.Register(SerializedBuffer{})
456 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
457 func (b *Buffer) SaveAs(filename string) error {
459 if b.Settings["rmtrailingws"].(bool) {
460 for i, l := range b.lines {
461 pos := len(bytes.TrimRightFunc(l.data, unicode.IsSpace))
463 if pos < len(l.data) {
464 b.deleteToEnd(Loc{pos, i})
471 if b.Settings["eofnewline"].(bool) {
473 if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
479 b.ModTime, _ = GetModTime(filename)
482 // Removes any tilde and replaces with the absolute path to home
483 absFilename := ReplaceHome(filename)
485 // Get the leading path to the file | "." is returned if there's no leading path provided
486 if dirname := filepath.Dir(absFilename); dirname != "." {
487 // Check if the parent dirs don't exist
488 if _, statErr := os.Stat(dirname); os.IsNotExist(statErr) {
489 // Prompt to make sure they want to create the dirs that are missing
490 if yes, canceled := messenger.YesNoPrompt("Parent folders \"" + dirname + "\" do not exist. Create them? (y,n)"); yes && !canceled {
491 // Create all leading dir(s) since they don't exist
492 if mkdirallErr := os.MkdirAll(dirname, os.ModePerm); mkdirallErr != nil {
493 // If there was an error creating the dirs
497 // If they canceled the creation of leading dirs
498 return errors.New("Save aborted")
505 err := overwriteFile(absFilename, func(file io.Writer) (e error) {
506 if len(b.lines) == 0 {
513 if b.Settings["fileformat"] == "dos" {
514 eol = []byte{'\r', '\n'}
520 if fileSize, e = file.Write(b.lines[0].data); e != nil {
524 for _, l := range b.lines[1:] {
525 if _, e = file.Write(eol); e != nil {
529 if _, e = file.Write(l.data); e != nil {
533 fileSize += len(eol) + len(l.data)
543 if !b.Settings["fastdirty"].(bool) {
544 if fileSize > LargeFileThreshold {
545 // For large files 'fastdirty' needs to be on
546 b.Settings["fastdirty"] = true
548 calcHash(b, &b.origHash)
557 // overwriteFile opens the given file for writing, truncating if one exists, and then calls
558 // the supplied function with the file as io.Writer object, also making sure the file is
559 // closed afterwards.
560 func overwriteFile(name string, fn func(io.Writer) error) (err error) {
563 if file, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil {
568 if e := file.Close(); e != nil && err == nil {
573 w := bufio.NewWriter(file)
575 if err = fn(w); err != nil {
583 // calcHash calculates md5 hash of all lines in the buffer
584 func calcHash(b *Buffer, out *[md5.Size]byte) {
587 if len(b.lines) > 0 {
588 h.Write(b.lines[0].data)
590 for _, l := range b.lines[1:] {
591 h.Write([]byte{'\n'})
599 // SaveAsWithSudo is the same as SaveAs except it uses a neat trick
600 // with tee to use sudo so the user doesn't have to reopen micro with sudo
601 func (b *Buffer) SaveAsWithSudo(filename string) error {
605 // Shut down the screen because we're going to interact directly with the shell
609 // Set up everything for the command
610 cmd := exec.Command(globalSettings["sucmd"].(string), "tee", filename)
611 cmd.Stdin = bytes.NewBufferString(b.SaveString(b.Settings["fileformat"] == "dos"))
613 // This is a trap for Ctrl-C so that it doesn't kill micro
614 // Instead we trap Ctrl-C to kill the program we're running
615 c := make(chan os.Signal, 1)
616 signal.Notify(c, os.Interrupt)
627 // Start the screen back up
631 b.ModTime, _ = GetModTime(filename)
637 // Modified returns if this buffer has been modified since
639 func (b *Buffer) Modified() bool {
640 if b.Settings["fastdirty"].(bool) {
644 var buff [md5.Size]byte
647 return buff != b.origHash
650 func (b *Buffer) insert(pos Loc, value []byte) {
652 b.LineArray.insert(pos, value)
655 func (b *Buffer) remove(start, end Loc) string {
657 sub := b.LineArray.remove(start, end)
661 func (b *Buffer) deleteToEnd(start Loc) {
663 b.LineArray.DeleteToEnd(start)
667 // Start returns the location of the first character in the buffer
668 func (b *Buffer) Start() Loc {
672 // End returns the location of the last character in the buffer
673 func (b *Buffer) End() Loc {
674 return Loc{utf8.RuneCount(b.lines[b.NumLines-1].data), b.NumLines - 1}
677 // RuneAt returns the rune at a given location in the buffer
678 func (b *Buffer) RuneAt(loc Loc) rune {
679 line := b.LineRunes(loc.Y)
686 // LineBytes returns a single line as an array of runes
687 func (b *Buffer) LineBytes(n int) []byte {
688 if n >= len(b.lines) {
691 return b.lines[n].data
694 // LineRunes returns a single line as an array of runes
695 func (b *Buffer) LineRunes(n int) []rune {
696 if n >= len(b.lines) {
699 return toRunes(b.lines[n].data)
702 // Line returns a single line
703 func (b *Buffer) Line(n int) string {
704 if n >= len(b.lines) {
707 return string(b.lines[n].data)
710 // LinesNum returns the number of lines in the buffer
711 func (b *Buffer) LinesNum() int {
715 // Lines returns an array of strings containing the lines from start to end
716 func (b *Buffer) Lines(start, end int) []string {
717 lines := b.lines[start:end]
719 for _, line := range lines {
720 slice = append(slice, string(line.data))
725 // Len gives the length of the buffer
726 func (b *Buffer) Len() (n int) {
727 for _, l := range b.lines {
728 n += utf8.RuneCount(l.data)
731 if len(b.lines) > 1 {
732 n += len(b.lines) - 1 // account for newlines
738 // MoveLinesUp moves the range of lines up one row
739 func (b *Buffer) MoveLinesUp(start int, end int) {
740 // 0 < start < end <= len(b.lines)
741 if start < 1 || start >= end || end > len(b.lines) {
742 return // what to do? FIXME
744 if end == len(b.lines) {
747 utf8.RuneCount(b.lines[end-1].data),
750 "\n"+b.Line(start-1),
755 b.Line(start-1)+"\n",
764 // MoveLinesDown moves the range of lines down one row
765 func (b *Buffer) MoveLinesDown(start int, end int) {
766 // 0 <= start < end < len(b.lines)
767 // if end == len(b.lines), we can't do anything here because the
768 // last line is unaccessible, FIXME
769 if start < 0 || start >= end || end >= len(b.lines)-1 {
770 return // what to do? FIXME
783 // ClearMatches clears all of the syntax highlighting for this buffer
784 func (b *Buffer) ClearMatches() {
785 for i := range b.lines {
791 func (b *Buffer) clearCursors() {
792 for i := 1; i < len(b.cursors); i++ {
795 b.cursors = b.cursors[:1]
797 b.Cursor.ResetSelection()
800 var bracePairs = [][2]rune{
806 // FindMatchingBrace returns the location in the buffer of the matching bracket
807 // It is given a brace type containing the open and closing character, (for example
808 // '{' and '}') as well as the location to match from
809 func (b *Buffer) FindMatchingBrace(braceType [2]rune, start Loc) Loc {
810 curLine := b.LineRunes(start.Y)
811 startChar := curLine[start.X]
813 if startChar == braceType[0] {
814 for y := start.Y; y < b.NumLines; y++ {
820 for x := xInit; x < len(l); x++ {
822 if r == braceType[0] {
824 } else if r == braceType[1] {
832 } else if startChar == braceType[1] {
833 for y := start.Y; y >= 0; y-- {
834 l := []rune(string(b.lines[y].data))
839 for x := xInit; x >= 0; x-- {
841 if r == braceType[0] {
846 } else if r == braceType[1] {