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
33 // Path to the file on disk
35 // Absolute path to the file on disk
37 // Name of the buffer on the status line
40 // Whether or not the buffer has been modified since it was opened
43 // Stores the last modification time of the file the buffer is pointing to
48 syntaxDef *highlight.Def
49 highlighter *highlight.Highlighter
51 // Buffer local settings
52 Settings map[string]interface{}
55 // The SerializedBuffer holds the types that get serialized when a buffer is saved
56 // These are used for the savecursor and saveundo options
57 type SerializedBuffer struct {
58 EventHandler *EventHandler
63 func NewBufferFromString(text, path string) *Buffer {
64 return NewBuffer(strings.NewReader(text), path)
67 // NewBuffer creates a new buffer from a given reader with a given path
68 func NewBuffer(reader io.Reader, path string) *Buffer {
70 for _, tab := range tabs {
71 for _, view := range tab.views {
72 if view.Buf.Path == path {
80 b.LineArray = NewLineArray(reader)
82 b.Settings = DefaultLocalSettings()
83 for k, v := range globalSettings {
84 if _, ok := b.Settings[k]; ok {
89 absPath, _ := filepath.Abs(path)
94 // The last time this file was modified
95 b.ModTime, _ = GetModTime(b.Path)
97 b.EventHandler = NewEventHandler(b)
102 if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
103 os.Mkdir(configDir+"/buffers/", os.ModePerm)
106 // Put the cursor at the first spot
109 // If -startpos LINE,COL was passed, use start position LINE,COL
110 if len(*flagStartPos) > 0 {
111 positions := strings.Split(*flagStartPos, ",")
112 if len(positions) == 2 {
113 lineNum, errPos1 := strconv.Atoi(positions[0])
114 colNum, errPos2 := strconv.Atoi(positions[1])
115 if errPos1 == nil && errPos2 == nil {
116 cursorStartX = colNum
117 cursorStartY = lineNum - 1
118 // Check to avoid line overflow
119 if cursorStartY > b.NumLines {
120 cursorStartY = b.NumLines - 1
121 } else if cursorStartY < 0 {
124 // Check to avoid column overflow
125 if cursorStartX > len(b.Line(cursorStartY)) {
126 cursorStartX = len(b.Line(cursorStartY))
127 } else if cursorStartX < 0 {
143 if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
144 // If either savecursor or saveundo is turned on, we need to load the serialized information
145 // from ~/.config/micro/buffers
146 file, err := os.Open(configDir + "/buffers/" + EscapePath(b.AbsPath))
148 var buffer SerializedBuffer
149 decoder := gob.NewDecoder(file)
150 gob.Register(TextEvent{})
151 err = decoder.Decode(&buffer)
153 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.")
155 if b.Settings["savecursor"].(bool) {
156 b.Cursor = buffer.Cursor
161 if b.Settings["saveundo"].(bool) {
162 // We should only use last time's eventhandler if the file wasn't by someone else in the meantime
163 if b.ModTime == buffer.ModTime {
164 b.EventHandler = buffer.EventHandler
165 b.EventHandler.buf = b
175 func (b *Buffer) GetName() string {
185 // UpdateRules updates the syntax rules and filetype for this buffer
186 // This is called when the colorscheme changes
187 func (b *Buffer) UpdateRules() {
188 b.syntaxDef = highlight.DetectFiletype(syntaxDefs, b.Path, []byte(b.Line(0)))
189 if b.highlighter == nil || b.Settings["filetype"].(string) != b.syntaxDef.FileType {
190 b.Settings["filetype"] = b.syntaxDef.FileType
191 b.highlighter = highlight.NewHighlighter(b.syntaxDef)
192 if b.Settings["syntax"].(bool) {
193 b.highlighter.HighlightStates(b)
198 // FileType returns the buffer's filetype
199 func (b *Buffer) FileType() string {
200 return b.Settings["filetype"].(string)
203 // IndentString returns a string representing one level of indentation
204 func (b *Buffer) IndentString() string {
205 if b.Settings["tabstospaces"].(bool) {
206 return Spaces(int(b.Settings["tabsize"].(float64)))
211 // CheckModTime makes sure that the file this buffer points to hasn't been updated
212 // by an external program since it was last read
213 // If it has, we ask the user if they would like to reload the file
214 func (b *Buffer) CheckModTime() {
215 modTime, ok := GetModTime(b.Path)
217 if modTime != b.ModTime {
218 choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
221 if !choice || canceled {
222 // Don't load new changes -- do nothing
223 b.ModTime, _ = GetModTime(b.Path)
232 // ReOpen reloads the current buffer from disk
233 func (b *Buffer) ReOpen() {
234 data, err := ioutil.ReadFile(b.Path)
238 messenger.Error(err.Error())
241 b.EventHandler.ApplyDiff(txt)
243 b.ModTime, _ = GetModTime(b.Path)
249 // Update fetches the string from the rope and updates the `text` and `lines` in the buffer
250 func (b *Buffer) Update() {
251 b.NumLines = len(b.lines)
254 // Save saves the buffer to its default path
255 func (b *Buffer) Save() error {
256 return b.SaveAs(b.Path)
259 // SaveWithSudo saves the buffer to the default path with sudo
260 func (b *Buffer) SaveWithSudo() error {
261 return b.SaveAsWithSudo(b.Path)
264 // Serialize serializes the buffer to configDir/buffers
265 func (b *Buffer) Serialize() error {
266 if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
267 file, err := os.Create(configDir + "/buffers/" + EscapePath(b.AbsPath))
269 enc := gob.NewEncoder(file)
270 gob.Register(TextEvent{})
271 err = enc.Encode(SerializedBuffer{
283 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
284 func (b *Buffer) SaveAs(filename string) error {
286 dir, _ := homedir.Dir()
287 b.Path = strings.Replace(filename, "~", dir, 1)
288 if b.Settings["rmtrailingws"].(bool) {
289 r, _ := regexp.Compile(`[ \t]+$`)
290 for lineNum, line := range b.Lines(0, b.NumLines) {
291 indices := r.FindStringIndex(line)
295 startLoc := Loc{indices[0], lineNum}
296 b.deleteToEnd(startLoc)
300 if b.Settings["eofnewline"].(bool) {
302 if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
308 err := ioutil.WriteFile(filename, data, 0644)
311 b.ModTime, _ = GetModTime(filename)
314 b.ModTime, _ = GetModTime(filename)
318 // SaveAsWithSudo is the same as SaveAs except it uses a neat trick
319 // with tee to use sudo so the user doesn't have to reopen micro with sudo
320 func (b *Buffer) SaveAsWithSudo(filename string) error {
324 // The user may have already used sudo in which case we won't need the password
325 // It's a bit nicer for them if they don't have to enter the password every time
326 _, err := RunShellCommand("sudo -v")
327 needPassword := err != nil
329 // If we need the password, we have to close the screen and ask using the shell
331 // Shut down the screen because we're going to interact directly with the shell
336 // Set up everything for the command
337 cmd := exec.Command("sudo", "tee", filename)
338 cmd.Stdin = bytes.NewBufferString(b.String())
340 // This is a trap for Ctrl-C so that it doesn't kill micro
341 // Instead we trap Ctrl-C to kill the program we're running
342 c := make(chan os.Signal, 1)
343 signal.Notify(c, os.Interrupt)
354 // If we needed the password, we closed the screen, so we have to initialize it again
356 // Start the screen back up
361 b.ModTime, _ = GetModTime(filename)
367 func (b *Buffer) insert(pos Loc, value []byte) {
369 b.LineArray.insert(pos, value)
372 func (b *Buffer) remove(start, end Loc) string {
374 sub := b.LineArray.remove(start, end)
378 func (b *Buffer) deleteToEnd(start Loc) {
380 b.LineArray.DeleteToEnd(start)
384 // Start returns the location of the first character in the buffer
385 func (b *Buffer) Start() Loc {
389 // End returns the location of the last character in the buffer
390 func (b *Buffer) End() Loc {
391 return Loc{utf8.RuneCount(b.lines[b.NumLines-1].data), b.NumLines - 1}
394 // RuneAt returns the rune at a given location in the buffer
395 func (b *Buffer) RuneAt(loc Loc) rune {
396 line := []rune(b.Line(loc.Y))
403 // Line returns a single line
404 func (b *Buffer) Line(n int) string {
405 if n >= len(b.lines) {
408 return string(b.lines[n].data)
411 func (b *Buffer) LinesNum() int {
415 // Lines returns an array of strings containing the lines from start to end
416 func (b *Buffer) Lines(start, end int) []string {
417 lines := b.lines[start:end]
419 for _, line := range lines {
420 slice = append(slice, string(line.data))
425 // Len gives the length of the buffer
426 func (b *Buffer) Len() int {
427 return Count(b.String())
430 // MoveLinesUp moves the range of lines up one row
431 func (b *Buffer) MoveLinesUp(start int, end int) {
432 // 0 < start < end <= len(b.lines)
433 if start < 1 || start >= end || end > len(b.lines) {
434 return // what to do? FIXME
436 if end == len(b.lines) {
439 utf8.RuneCount(b.lines[end-1].data),
442 "\n"+b.Line(start-1),
447 b.Line(start-1)+"\n",
456 // MoveLinesDown moves the range of lines down one row
457 func (b *Buffer) MoveLinesDown(start int, end int) {
458 // 0 <= start < end < len(b.lines)
459 // if end == len(b.lines), we can't do anything here because the
460 // last line is unaccessible, FIXME
461 if start < 0 || start >= end || end >= len(b.lines)-1 {
462 return // what to do? FIXME
475 // ClearMatches clears all of the syntax highlighting for this buffer
476 func (b *Buffer) ClearMatches() {
477 for i := range b.lines {