17 // Buffer stores the text for files that are loaded into the text editor
18 // It uses a rope to efficiently store the string and contains some
19 // simple functions for saving and wrapper functions for modifying the rope
21 // The eventhandler for undo/redo
23 // This stores all the text in the buffer as an array of lines
28 // Path to the file on disk
30 // Name of the buffer on the status line
33 // Whether or not the buffer has been modified since it was opened
36 // Stores the last modification time of the file the buffer is pointing to
41 // Syntax highlighting rules
44 // Buffer local settings
45 Settings map[string]interface{}
48 // The SerializedBuffer holds the types that get serialized when a buffer is saved
49 // These are used for the savecursor and saveundo options
50 type SerializedBuffer struct {
51 EventHandler *EventHandler
56 // NewBuffer creates a new buffer from `txt` with path and name `path`
57 func NewBuffer(txt []byte, path string) *Buffer {
59 for _, tab := range tabs {
60 for _, view := range tab.views {
61 if view.Buf.Path == path {
69 b.LineArray = NewLineArray(txt)
71 b.Settings = DefaultLocalSettings()
72 for k, v := range globalSettings {
73 if _, ok := b.Settings[k]; ok {
81 // If the file doesn't have a path to disk then we give it no name
86 // The last time this file was modified
87 b.ModTime, _ = GetModTime(b.Path)
89 b.EventHandler = NewEventHandler(b)
95 if _, err := os.Stat(configDir + "/buffers/"); os.IsNotExist(err) {
96 os.Mkdir(configDir+"/buffers/", os.ModePerm)
99 // Put the cursor at the first spot
102 // If -startpos LINE,COL was passed, use start position LINE,COL
103 if len(*flagStartPos) > 0 {
104 positions := strings.Split(*flagStartPos, ",")
105 if len(positions) == 2 {
106 lineNum, errPos1 := strconv.Atoi(positions[0])
107 colNum, errPos2 := strconv.Atoi(positions[1])
108 if errPos1 == nil && errPos2 == nil {
109 cursorStartX = colNum
110 cursorStartY = lineNum - 1
111 // Check to avoid line overflow
112 if cursorStartY > b.NumLines {
113 cursorStartY = b.NumLines - 1
114 } else if cursorStartY < 0 {
117 // Check to avoid column overflow
118 if cursorStartX > len(b.Line(cursorStartY)) {
119 cursorStartX = len(b.Line(cursorStartY))
120 } else if cursorStartX < 0 {
136 if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
137 // If either savecursor or saveundo is turned on, we need to load the serialized information
138 // from ~/.config/micro/buffers
139 absPath, _ := filepath.Abs(b.Path)
140 file, err := os.Open(configDir + "/buffers/" + EscapePath(absPath))
142 var buffer SerializedBuffer
143 decoder := gob.NewDecoder(file)
144 gob.Register(TextEvent{})
145 err = decoder.Decode(&buffer)
147 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.")
149 if b.Settings["savecursor"].(bool) {
150 b.Cursor = buffer.Cursor
155 if b.Settings["saveundo"].(bool) {
156 // We should only use last time's eventhandler if the file wasn't by someone else in the meantime
157 if b.ModTime == buffer.ModTime {
158 b.EventHandler = buffer.EventHandler
159 b.EventHandler.buf = b
169 // UpdateRules updates the syntax rules and filetype for this buffer
170 // This is called when the colorscheme changes
171 func (b *Buffer) UpdateRules() {
172 b.rules = GetRules(b)
175 // FindFileType identifies this buffer's filetype based on the extension or header
176 func (b *Buffer) FindFileType() {
177 b.Settings["filetype"] = FindFileType(b)
180 // FileType returns the buffer's filetype
181 func (b *Buffer) FileType() string {
182 return b.Settings["filetype"].(string)
185 // IndentString returns a string representing one level of indentation
186 func (b *Buffer) IndentString() string {
187 if b.Settings["tabstospaces"].(bool) {
188 return Spaces(int(b.Settings["tabsize"].(float64)))
193 // CheckModTime makes sure that the file this buffer points to hasn't been updated
194 // by an external program since it was last read
195 // If it has, we ask the user if they would like to reload the file
196 func (b *Buffer) CheckModTime() {
197 modTime, ok := GetModTime(b.Path)
199 if modTime != b.ModTime {
200 choice, canceled := messenger.YesNoPrompt("The file has changed since it was last read. Reload file? (y,n)")
203 if !choice || canceled {
204 // Don't load new changes -- do nothing
205 b.ModTime, _ = GetModTime(b.Path)
214 // ReOpen reloads the current buffer from disk
215 func (b *Buffer) ReOpen() {
216 data, err := ioutil.ReadFile(b.Path)
220 messenger.Error(err.Error())
223 b.EventHandler.ApplyDiff(txt)
225 b.ModTime, _ = GetModTime(b.Path)
231 // Update fetches the string from the rope and updates the `text` and `lines` in the buffer
232 func (b *Buffer) Update() {
233 b.NumLines = len(b.lines)
236 // Save saves the buffer to its default path
237 func (b *Buffer) Save() error {
238 return b.SaveAs(b.Path)
241 // SaveWithSudo saves the buffer to the default path with sudo
242 func (b *Buffer) SaveWithSudo() error {
243 return b.SaveAsWithSudo(b.Path)
246 // Serialize serializes the buffer to configDir/buffers
247 func (b *Buffer) Serialize() error {
248 if b.Settings["savecursor"].(bool) || b.Settings["saveundo"].(bool) {
249 absPath, _ := filepath.Abs(b.Path)
250 file, err := os.Create(configDir + "/buffers/" + EscapePath(absPath))
252 enc := gob.NewEncoder(file)
253 gob.Register(TextEvent{})
254 err = enc.Encode(SerializedBuffer{
266 // SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
267 func (b *Buffer) SaveAs(filename string) error {
273 if b.Settings["eofnewline"].(bool) {
275 if b.RuneAt(Loc{end.X - 1, end.Y}) != '\n' {
280 err := ioutil.WriteFile(filename, data, 0644)
283 b.ModTime, _ = GetModTime(filename)
286 b.ModTime, _ = GetModTime(filename)
290 // SaveAsWithSudo is the same as SaveAs except it uses a neat trick
291 // with tee to use sudo so the user doesn't have to reopen micro with sudo
292 func (b *Buffer) SaveAsWithSudo(filename string) error {
298 // The user may have already used sudo in which case we won't need the password
299 // It's a bit nicer for them if they don't have to enter the password every time
300 _, err := RunShellCommand("sudo -v")
301 needPassword := err != nil
303 // If we need the password, we have to close the screen and ask using the shell
305 // Shut down the screen because we're going to interact directly with the shell
310 // Set up everything for the command
311 cmd := exec.Command("sudo", "tee", filename)
312 cmd.Stdin = bytes.NewBufferString(b.String())
314 // This is a trap for Ctrl-C so that it doesn't kill micro
315 // Instead we trap Ctrl-C to kill the program we're running
316 c := make(chan os.Signal, 1)
317 signal.Notify(c, os.Interrupt)
328 // If we needed the password, we closed the screen, so we have to initialize it again
330 // Start the screen back up
335 b.ModTime, _ = GetModTime(filename)
341 func (b *Buffer) insert(pos Loc, value []byte) {
343 b.LineArray.insert(pos, value)
346 func (b *Buffer) remove(start, end Loc) string {
348 sub := b.LineArray.remove(start, end)
353 // Start returns the location of the first character in the buffer
354 func (b *Buffer) Start() Loc {
358 // End returns the location of the last character in the buffer
359 func (b *Buffer) End() Loc {
360 return Loc{utf8.RuneCount(b.lines[b.NumLines-1]), b.NumLines - 1}
363 // RuneAt returns the rune at a given location in the buffer
364 func (b *Buffer) RuneAt(loc Loc) rune {
365 line := []rune(b.Line(loc.Y))
372 // Line returns a single line
373 func (b *Buffer) Line(n int) string {
374 if n >= len(b.lines) {
377 return string(b.lines[n])
380 // Lines returns an array of strings containing the lines from start to end
381 func (b *Buffer) Lines(start, end int) []string {
382 lines := b.lines[start:end]
384 for _, line := range lines {
385 slice = append(slice, string(line))
390 // Len gives the length of the buffer
391 func (b *Buffer) Len() int {
392 return Count(b.String())
395 // MoveLinesUp moves the range of lines up one row
396 func (b *Buffer) MoveLinesUp(start int, end int) {
397 // 0 < start < end <= len(b.lines)
398 if start < 1 || start >= end || end > len(b.lines) {
399 return // what to do? FIXME
401 if end == len(b.lines) {
404 utf8.RuneCount(b.lines[end-1]),
407 "\n"+b.Line(start-1),
412 b.Line(start-1)+"\n",
421 // MoveLinesDown moves the range of lines down one row
422 func (b *Buffer) MoveLinesDown(start int, end int) {
423 // 0 <= start < end < len(b.lines)
424 // if end == len(b.lines), we can't do anything here because the
425 // last line is unaccessible, FIXME
426 if start < 0 || start >= end || end >= len(b.lines)-1 {
427 return // what to do? FIXME