19 "github.com/zyedidia/micro/cmd/micro/highlight"
23 // 0 - no line type detected
29 // Buffer stores the text for files that are loaded into the text editor
30 // It uses a rope to efficiently store the string and contains some
31 // simple functions for saving and wrapper functions for modifying the rope
33 // The eventhandler for undo/redo
35 // This stores all the text in the buffer as an array of lines
39 cursors []*Cursor // for multiple cursors
40 curCursor int // the current cursor
42 // Path to the file on disk
44 // Absolute path to the file on disk
46 // Name of the buffer on the status line
49 // Whether or not the buffer has been modified since it was opened
52 // Stores the last modification time of the file the buffer is pointing to
57 syntaxDef *highlight.Def
58 highlighter *highlight.Highlighter
60 // Hash of the original buffer -- empty if fastdirty is on
63 // Buffer local settings
64 Settings map[string]interface{}
67 // The SerializedBuffer holds the types that get serialized when a buffer is saved
68 // These are used for the savecursor and saveundo options
69 type SerializedBuffer struct {
70 EventHandler *EventHandler
75 func NewBufferFromString(text, path string) *Buffer {
76 return NewBuffer(strings.NewReader(text), int64(len(text)), path)
79 // NewBuffer creates a new buffer from a given reader with a given path
80 func NewBuffer(reader io.Reader, size int64, path string) *Buffer {
82 for _, tab := range tabs {
83 for _, view := range tab.views {
84 if view.Buf.Path == path {
92 b.LineArray = NewLineArray(size, reader)
94 b.Settings = DefaultLocalSettings()
95 for k, v := range globalSettings {
96 if _, ok := b.Settings[k]; ok {
102 b.Settings["fileformat"] = "unix"
103 } else if fileformat == 2 {
104 b.Settings["fileformat"] = "dos"
107 absPath, _ := filepath.Abs(path)
112 // The last time this file was modified
113 b.ModTime, _ = GetModTime(b.Path)
115 b.EventHandler = NewEventHandler(b)
120 if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
121 os.Mkdir(configDir+"/buffers/", os.ModePerm)
124 // Put the cursor at the first spot
127 // If -startpos LINE,COL was passed, use start position LINE,COL
128 if len(*flagStartPos) > 0 {
129 positions := strings.Split(*flagStartPos, ",")
130 if len(positions) == 2 {
131 lineNum, errPos1 := strconv.Atoi(positions[0])
132 colNum, errPos2 := strconv.Atoi(positions[1])
133 if errPos1 == nil && errPos2 == nil {
134 cursorStartX = colNum
135 cursorStartY = lineNum - 1
136 // Check to avoid line overflow
137 if cursorStartY > b.NumLines {
138 cursorStartY = b.NumLines - 1
139 } else if cursorStartY < 0 {
142 // Check to avoid column overflow
143 if cursorStartX > len(b.Line(cursorStartY)) {
144 cursorStartX = len(b.Line(cursorStartY))
145 } else if cursorStartX < 0 {
161 if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
162 // If either savecursor or saveundo is turned on, we need to load the serialized information
163 // from ~/.config/micro/buffers
164 file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath))
166 var buffer SerializedBuffer
167 decoder := gob.NewDecoder(file)
168 gob.Register(TextEvent{})
169 err = decoder.Decode(&buffer)
171 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.")
173 if b.Settings["savecursor"].(bool) {
174 b.Cursor = buffer.Cursor
179 if b.Settings["saveundo"].(bool) {
180 // We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
181 if b.ModTime == buffer.ModTime {
182 b.EventHandler = buffer.EventHandler
183 b.EventHandler.buf = b
190 if !b.Settings["fastdirty"].(bool) {
192 // If the file is larger than a megabyte fastdirty needs to be on
193 b.Settings["fastdirty"] = true
195 b.origHash = md5.Sum([]byte(b.String()))
199 b.cursors = []*Cursor{&b.Cursor}
204 func (b *Buffer) GetName() string {
214 // UpdateRules updates the syntax rules and filetype for this buffer
215 // This is called when the colorscheme changes
216 func (b *Buffer) UpdateRules() {
218 var files []*highlight.File
219 for _, f := range ListRuntimeFiles(RTSyntax) {
220 data, err := f.Data()
222 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
224 file, err := highlight.ParseFile(data)
226 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
229 ftdetect, err := highlight.ParseFtDetect(file)
231 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
235 ft := b.Settings["filetype"].(string)
236 if (ft == "Unknown" || ft == "") && !rehighlight {
237 if highlight.MatchFiletype(ftdetect, b.Path, b.lines[0].data) {
238 header := new(highlight.Header)
239 header.FileType = file.FileType
240 header.FtDetect = ftdetect
241 b.syntaxDef, err = highlight.ParseDef(file, header)
243 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
249 if file.FileType == ft && !rehighlight {
250 header := new(highlight.Header)
251 header.FileType = file.FileType
252 header.FtDetect = ftdetect
253 b.syntaxDef, err = highlight.ParseDef(file, header)
255 TermMessage("Error loading syntax file " + f.Name() + ": " + err.Error())
261 files = append(files, file)
265 if b.syntaxDef != nil {
266 highlight.ResolveIncludes(b.syntaxDef, files)
269 if b.highlighter == nil || rehighlight {
270 if b.syntaxDef != nil {
271 b.Settings["filetype"] = b.syntaxDef.FileType
272 b.highlighter = highlight.NewHighlighter(b.syntaxDef)
273 if b.Settings["syntax"].(bool) {
274 b.highlighter.HighlightStates(b)
280 // FileType returns the buffer's filetype
281 func (b *Buffer) FileType() string {
282 return b.Settings["filetype"].(string)
285 // IndentString returns a string representing one level of indentation
286 func (b *Buffer) IndentString() string {
287 if b.Settings["tabstospaces"].(bool) {
288 return Spaces(int(b.Settings["tabsize"].(float64)))
293 // CheckModTime makes sure that the file this buffer points to hasn't been updated
294 // by an external program since it was last read
295 // If it has, we ask the user if they would like to reload the file
296 func (b *Buffer) CheckModTime() {
297 modTime, ok := GetModTime(b.Path)
299 if modTime != b.ModTime {
300 choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
303 if !choice || canceled {
304 // Don't load new changes -- do nothing
305 b.ModTime, _ = GetModTime(b.Path)
314 // ReOpen reloads the current buffer from disk
315 func (b *Buffer) ReOpen() {
316 data, err := ioutil.ReadFile(b.Path)
320 messenger.Error(err.Error())
323 b.EventHandler.ApplyDiff(txt)
325 b.ModTime, _ = GetModTime(b.Path)
331 // Update fetches the string from the rope and updates the `text` and `lines` in the buffer
332 func (b *Buffer) Update() {
333 b.NumLines = len(b.lines)
336 func (b *Buffer) MergeCursors() {
337 var cursors []*Cursor
338 for i := 0; i < len(b.cursors); i++ {
341 for j := 0; j < len(b.cursors); j++ {
343 if c2 != nil && i != j && c1.Loc == c2.Loc {
347 cursors = append(cursors, c1)
354 func (b *Buffer) UpdateCursors() {
355 for i, c := range b.cursors {
360 // Save saves the buffer to its default path
361 func (b *Buffer) Save() error {
362 return b.SaveAs(b.Path)
365 // SaveWithSudo saves the buffer to the default path with sudo
366 func (b *Buffer) SaveWithSudo() error {
367 return b.SaveAsWithSudo(b.Path)
370 // Serialize serializes the buffer to configDir/buffers
371 func (b *Buffer) Serialize() error {
372 if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
373 file, err := os.Create(configDir + "/buffers/" + EscapePath(b.AbsPath))
375 enc := gob.NewEncoder(file)
376 gob.Register(TextEvent{})
377 err = enc.Encode(SerializedBuffer{
389 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
390 func (b *Buffer) SaveAs(filename string) error {
392 if b.Settings["rmtrailingws"].(bool) {
393 r, _ := regexp.Compile(`[ \t]+$`)
394 for lineNum, line := range b.Lines(0, b.NumLines) {
395 indices := r.FindStringIndex(line)
399 startLoc := Loc{indices[0], lineNum}
400 b.deleteToEnd(startLoc)
404 if b.Settings["eofnewline"].(bool) {
406 if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
410 str := b.SaveString(b.Settings["fileformat"] == "dos")
412 err := ioutil.WriteFile(ReplaceHome(filename), data, 0644)
416 b.ModTime, _ = GetModTime(filename)
419 b.ModTime, _ = GetModTime(filename)
423 // SaveAsWithSudo is the same as SaveAs except it uses a neat trick
424 // with tee to use sudo so the user doesn't have to reopen micro with sudo
425 func (b *Buffer) SaveAsWithSudo(filename string) error {
429 // Shut down the screen because we're going to interact directly with the shell
433 // Set up everything for the command
434 cmd := exec.Command(b.Settings["sucmd"].(string), "tee", filename)
435 cmd.Stdin = bytes.NewBufferString(b.SaveString(b.Settings["fileformat"] == "dos"))
437 // This is a trap for Ctrl-C so that it doesn't kill micro
438 // Instead we trap Ctrl-C to kill the program we're running
439 c := make(chan os.Signal, 1)
440 signal.Notify(c, os.Interrupt)
451 // Start the screen back up
455 b.ModTime, _ = GetModTime(filename)
461 func (b *Buffer) Modified() bool {
462 if b.Settings["fastdirty"].(bool) {
465 return b.origHash != md5.Sum([]byte(b.String()))
468 func (b *Buffer) insert(pos Loc, value []byte) {
470 b.LineArray.insert(pos, value)
473 func (b *Buffer) remove(start, end Loc) string {
475 sub := b.LineArray.remove(start, end)
479 func (b *Buffer) deleteToEnd(start Loc) {
481 b.LineArray.DeleteToEnd(start)
485 // Start returns the location of the first character in the buffer
486 func (b *Buffer) Start() Loc {
490 // End returns the location of the last character in the buffer
491 func (b *Buffer) End() Loc {
492 return Loc{utf8.RuneCount(b.lines[b.NumLines-1].data), b.NumLines - 1}
495 // RuneAt returns the rune at a given location in the buffer
496 func (b *Buffer) RuneAt(loc Loc) rune {
497 line := []rune(b.Line(loc.Y))
504 // Line returns a single line
505 func (b *Buffer) Line(n int) string {
506 if n >= len(b.lines) {
509 return string(b.lines[n].data)
512 func (b *Buffer) LinesNum() int {
516 // Lines returns an array of strings containing the lines from start to end
517 func (b *Buffer) Lines(start, end int) []string {
518 lines := b.lines[start:end]
520 for _, line := range lines {
521 slice = append(slice, string(line.data))
526 // Len gives the length of the buffer
527 func (b *Buffer) Len() int {
528 return Count(b.String())
531 // MoveLinesUp moves the range of lines up one row
532 func (b *Buffer) MoveLinesUp(start int, end int) {
533 // 0 < start < end <= len(b.lines)
534 if start < 1 || start >= end || end > len(b.lines) {
535 return // what to do? FIXME
537 if end == len(b.lines) {
540 utf8.RuneCount(b.lines[end-1].data),
543 "\n"+b.Line(start-1),
548 b.Line(start-1)+"\n",
557 // MoveLinesDown moves the range of lines down one row
558 func (b *Buffer) MoveLinesDown(start int, end int) {
559 // 0 <= start < end < len(b.lines)
560 // if end == len(b.lines), we can't do anything here because the
561 // last line is unaccessible, FIXME
562 if start < 0 || start >= end || end >= len(b.lines)-1 {
563 return // what to do? FIXME
576 // ClearMatches clears all of the syntax highlighting for this buffer
577 func (b *Buffer) ClearMatches() {
578 for i := range b.lines {
584 func (b *Buffer) clearCursors() {
585 for i := 1; i < len(b.cursors); i++ {
588 b.cursors = b.cursors[:1]
590 b.Cursor.ResetSelection()