18 "github.com/mitchellh/go-homedir"
19 "github.com/zyedidia/micro/cmd/micro/highlight"
22 // Buffer stores the text for files that are loaded into the text editor
23 // It uses a rope to efficiently store the string and contains some
24 // simple functions for saving and wrapper functions for modifying the rope
26 // The eventhandler for undo/redo
28 // This stores all the text in the buffer as an array of lines
32 cursors []*Cursor // for multiple cursors
33 curCursor int // the current cursor
35 // Path to the file on disk
37 // Absolute path to the file on disk
39 // Name of the buffer on the status line
42 // Whether or not the buffer has been modified since it was opened
45 // Stores the last modification time of the file the buffer is pointing to
50 syntaxDef *highlight.Def
51 highlighter *highlight.Highlighter
53 // Buffer local settings
54 Settings map[string]interface{}
57 // The SerializedBuffer holds the types that get serialized when a buffer is saved
58 // These are used for the savecursor and saveundo options
59 type SerializedBuffer struct {
60 EventHandler *EventHandler
65 func NewBufferFromString(text, path string) *Buffer {
66 return NewBuffer(strings.NewReader(text), int64(len(text)), path)
69 // NewBuffer creates a new buffer from a given reader with a given path
70 func NewBuffer(reader io.Reader, size int64, path string) *Buffer {
72 for _, tab := range tabs {
73 for _, view := range tab.views {
74 if view.Buf.Path == path {
82 b.LineArray = NewLineArray(size, reader)
84 b.Settings = DefaultLocalSettings()
85 for k, v := range globalSettings {
86 if _, ok := b.Settings[k]; ok {
91 absPath, _ := filepath.Abs(path)
96 // The last time this file was modified
97 b.ModTime, _ = GetModTime(b.Path)
99 b.EventHandler = NewEventHandler(b)
104 if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
105 os.Mkdir(configDir+"/buffers/", os.ModePerm)
108 // Put the cursor at the first spot
111 // If -startpos LINE,COL was passed, use start position LINE,COL
112 if len(*flagStartPos) > 0 {
113 positions := strings.Split(*flagStartPos, ",")
114 if len(positions) == 2 {
115 lineNum, errPos1 := strconv.Atoi(positions[0])
116 colNum, errPos2 := strconv.Atoi(positions[1])
117 if errPos1 == nil && errPos2 == nil {
118 cursorStartX = colNum
119 cursorStartY = lineNum - 1
120 // Check to avoid line overflow
121 if cursorStartY > b.NumLines {
122 cursorStartY = b.NumLines - 1
123 } else if cursorStartY < 0 {
126 // Check to avoid column overflow
127 if cursorStartX > len(b.Line(cursorStartY)) {
128 cursorStartX = len(b.Line(cursorStartY))
129 } else if cursorStartX < 0 {
145 if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
146 // If either savecursor or saveundo is turned on, we need to load the serialized information
147 // from ~/.config/micro/buffers
148 file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath))
150 var buffer SerializedBuffer
151 decoder := gob.NewDecoder(file)
152 gob.Register(TextEvent{})
153 err = decoder.Decode(&buffer)
155 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.")
157 if b.Settings["savecursor"].(bool) {
158 b.Cursor = buffer.Cursor
163 if b.Settings["saveundo"].(bool) {
164 // We should only use last time's eventhandler if the file wasn't by someone else in the meantime
165 if b.ModTime == buffer.ModTime {
166 b.EventHandler = buffer.EventHandler
167 b.EventHandler.buf = b
174 b.cursors = []*Cursor{&b.Cursor}
179 func (b *Buffer) GetName() string {
189 // UpdateRules updates the syntax rules and filetype for this buffer
190 // This is called when the colorscheme changes
191 func (b *Buffer) UpdateRules() {
193 var files []*highlight.File
194 for _, f := range ListRuntimeFiles(RTSyntax) {
195 data, err := f.Data()
197 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
199 file, err := highlight.ParseFile(data)
201 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
204 ftdetect, err := highlight.ParseFtDetect(file)
206 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
210 ft := b.Settings["filetype"].(string)
211 if ft == "Unknown" || ft == "" {
212 if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
213 header := new(highlight.Header)
214 header.FileType = file.FileType
215 header.FtDetect = ftdetect
216 b.syntaxDef, err = highlight.ParseDef(file, header)
218 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
224 if file.FileType == ft {
225 header := new(highlight.Header)
226 header.FileType = file.FileType
227 header.FtDetect = ftdetect
228 b.syntaxDef, err = highlight.ParseDef(file, header)
230 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
236 files = append(files, file)
240 if b.syntaxDef != nil {
241 highlight.ResolveIncludes(b.syntaxDef, files)
245 if b.highlighter == nil || rehighlight {
246 if b.syntaxDef != nil {
247 b.Settings["filetype"] = b.syntaxDef.FileType
248 b.highlighter = highlight.NewHighlighter(b.syntaxDef)
249 if b.Settings["syntax"].(bool) {
250 b.highlighter.HighlightStates(b)
256 // FileType returns the buffer's filetype
257 func (b *Buffer) FileType() string {
258 return b.Settings["filetype"].(string)
261 // IndentString returns a string representing one level of indentation
262 func (b *Buffer) IndentString() string {
263 if b.Settings["tabstospaces"].(bool) {
264 return Spaces(int(b.Settings["tabsize"].(float64)))
269 // CheckModTime makes sure that the file this buffer points to hasn't been updated
270 // by an external program since it was last read
271 // If it has, we ask the user if they would like to reload the file
272 func (b *Buffer) CheckModTime() {
273 modTime, ok := GetModTime(b.Path)
275 if modTime != b.ModTime {
276 choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
279 if !choice || canceled {
280 // Don't load new changes -- do nothing
281 b.ModTime, _ = GetModTime(b.Path)
290 // ReOpen reloads the current buffer from disk
291 func (b *Buffer) ReOpen() {
292 data, err := ioutil.ReadFile(b.Path)
296 messenger.Error(err.Error())
299 b.EventHandler.ApplyDiff(txt)
301 b.ModTime, _ = GetModTime(b.Path)
307 // Update fetches the string from the rope and updates the `text` and `lines` in the buffer
308 func (b *Buffer) Update() {
309 b.NumLines = len(b.lines)
312 func (b *Buffer) MergeCursors() {
313 var cursors []*Cursor
314 for i := 0; i < len(b.cursors); i++ {
317 for j := 0; j < len(b.cursors); j++ {
319 if c2 != nil && i != j && c1.Loc == c2.Loc {
323 cursors = append(cursors, c1)
330 func (b *Buffer) UpdateCursors() {
331 for i, c := range b.cursors {
336 // Save saves the buffer to its default path
337 func (b *Buffer) Save() error {
338 return b.SaveAs(b.Path)
341 // SaveWithSudo saves the buffer to the default path with sudo
342 func (b *Buffer) SaveWithSudo() error {
343 return b.SaveAsWithSudo(b.Path)
346 // Serialize serializes the buffer to configDir/buffers
347 func (b *Buffer) Serialize() error {
348 if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
349 file, err := os.Create(configDir + "/buffers/" + EscapePath(b.AbsPath))
351 enc := gob.NewEncoder(file)
352 gob.Register(TextEvent{})
353 err = enc.Encode(SerializedBuffer{
365 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
366 func (b *Buffer) SaveAs(filename string) error {
368 dir, _ := homedir.Dir()
369 if b.Settings["rmtrailingws"].(bool) {
370 r, _ := regexp.Compile(`[ \t]+$`)
371 for lineNum, line := range b.Lines(0, b.NumLines) {
372 indices := r.FindStringIndex(line)
376 startLoc := Loc{indices[0], lineNum}
377 b.deleteToEnd(startLoc)
381 if b.Settings["eofnewline"].(bool) {
383 if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
389 err := ioutil.WriteFile(filename, data, 0644)
391 b.Path = strings.Replace(filename, "~", dir, 1)
393 b.ModTime, _ = GetModTime(filename)
396 b.ModTime, _ = GetModTime(filename)
400 // SaveAsWithSudo is the same as SaveAs except it uses a neat trick
401 // with tee to use sudo so the user doesn't have to reopen micro with sudo
402 func (b *Buffer) SaveAsWithSudo(filename string) error {
406 // Shut down the screen because we're going to interact directly with the shell
410 // Set up everything for the command
411 cmd := exec.Command("sudo", "tee", filename)
412 cmd.Stdin = bytes.NewBufferString(b.String())
414 // This is a trap for Ctrl-C so that it doesn't kill micro
415 // Instead we trap Ctrl-C to kill the program we're running
416 c := make(chan os.Signal, 1)
417 signal.Notify(c, os.Interrupt)
428 // Start the screen back up
432 b.ModTime, _ = GetModTime(filename)
438 func (b *Buffer) insert(pos Loc, value []byte) {
440 b.LineArray.insert(pos, value)
443 func (b *Buffer) remove(start, end Loc) string {
445 sub := b.LineArray.remove(start, end)
449 func (b *Buffer) deleteToEnd(start Loc) {
451 b.LineArray.DeleteToEnd(start)
455 // Start returns the location of the first character in the buffer
456 func (b *Buffer) Start() Loc {
460 // End returns the location of the last character in the buffer
461 func (b *Buffer) End() Loc {
462 return Loc{utf8.RuneCount(b.lines[b.NumLines-1].data), b.NumLines - 1}
465 // RuneAt returns the rune at a given location in the buffer
466 func (b *Buffer) RuneAt(loc Loc) rune {
467 line := []rune(b.Line(loc.Y))
474 // Line returns a single line
475 func (b *Buffer) Line(n int) string {
476 if n >= len(b.lines) {
479 return string(b.lines[n].data)
482 func (b *Buffer) LinesNum() int {
486 // Lines returns an array of strings containing the lines from start to end
487 func (b *Buffer) Lines(start, end int) []string {
488 lines := b.lines[start:end]
490 for _, line := range lines {
491 slice = append(slice, string(line.data))
496 // Len gives the length of the buffer
497 func (b *Buffer) Len() int {
498 return Count(b.String())
501 // MoveLinesUp moves the range of lines up one row
502 func (b *Buffer) MoveLinesUp(start int, end int) {
503 // 0 < start < end <= len(b.lines)
504 if start < 1 || start >= end || end > len(b.lines) {
505 return // what to do? FIXME
507 if end == len(b.lines) {
510 utf8.RuneCount(b.lines[end-1].data),
513 "\n"+b.Line(start-1),
518 b.Line(start-1)+"\n",
527 // MoveLinesDown moves the range of lines down one row
528 func (b *Buffer) MoveLinesDown(start int, end int) {
529 // 0 <= start < end < len(b.lines)
530 // if end == len(b.lines), we can't do anything here because the
531 // last line is unaccessible, FIXME
532 if start < 0 || start >= end || end >= len(b.lines)-1 {
533 return // what to do? FIXME
546 // ClearMatches clears all of the syntax highlighting for this buffer
547 func (b *Buffer) ClearMatches() {
548 for i := range b.lines {