10 "github.com/atotto/clipboard"
11 "github.com/mitchellh/go-homedir"
14 // CursorUp moves the cursor up
15 func (v *View) CursorUp() bool {
16 if v.Cursor.HasSelection() {
17 v.Cursor.Loc = v.Cursor.CurSelection[0]
18 v.Cursor.ResetSelection()
24 // CursorDown moves the cursor down
25 func (v *View) CursorDown() bool {
26 if v.Cursor.HasSelection() {
27 v.Cursor.Loc = v.Cursor.CurSelection[1]
28 v.Cursor.ResetSelection()
34 // CursorLeft moves the cursor left
35 func (v *View) CursorLeft() bool {
36 if v.Cursor.HasSelection() {
37 v.Cursor.Loc = v.Cursor.CurSelection[0]
38 v.Cursor.ResetSelection()
45 // CursorRight moves the cursor right
46 func (v *View) CursorRight() bool {
47 if v.Cursor.HasSelection() {
48 v.Cursor.Loc = v.Cursor.CurSelection[1].Move(-1, v.Buf)
49 v.Cursor.ResetSelection()
56 // WordRight moves the cursor one word to the right
57 func (v *View) WordRight() bool {
62 // WordLeft moves the cursor one word to the left
63 func (v *View) WordLeft() bool {
68 // SelectUp selects up one line
69 func (v *View) SelectUp() bool {
70 if !v.Cursor.HasSelection() {
71 v.Cursor.OrigSelection[0] = v.Cursor.Loc
74 v.Cursor.SelectTo(v.Cursor.Loc)
78 // SelectDown selects down one line
79 func (v *View) SelectDown() bool {
80 if !v.Cursor.HasSelection() {
81 v.Cursor.OrigSelection[0] = v.Cursor.Loc
84 v.Cursor.SelectTo(v.Cursor.Loc)
88 // SelectLeft selects the character to the left of the cursor
89 func (v *View) SelectLeft() bool {
91 count := v.Buf.End().Move(-1, v.Buf)
92 if loc.GreaterThan(count) {
95 if !v.Cursor.HasSelection() {
96 v.Cursor.OrigSelection[0] = loc
99 v.Cursor.SelectTo(v.Cursor.Loc)
103 // SelectRight selects the character to the right of the cursor
104 func (v *View) SelectRight() bool {
106 count := v.Buf.End().Move(-1, v.Buf)
107 if loc.GreaterThan(count) {
110 if !v.Cursor.HasSelection() {
111 v.Cursor.OrigSelection[0] = loc
114 v.Cursor.SelectTo(v.Cursor.Loc)
118 // SelectWordRight selects the word to the right of the cursor
119 func (v *View) SelectWordRight() bool {
120 if !v.Cursor.HasSelection() {
121 v.Cursor.OrigSelection[0] = v.Cursor.Loc
124 v.Cursor.SelectTo(v.Cursor.Loc)
128 // SelectWordLeft selects the word to the left of the cursor
129 func (v *View) SelectWordLeft() bool {
130 if !v.Cursor.HasSelection() {
131 v.Cursor.OrigSelection[0] = v.Cursor.Loc
134 v.Cursor.SelectTo(v.Cursor.Loc)
138 // StartOfLine moves the cursor to the start of the line
139 func (v *View) StartOfLine() bool {
144 // EndOfLine moves the cursor to the end of the line
145 func (v *View) EndOfLine() bool {
150 // SelectToStartOfLine selects to the start of the current line
151 func (v *View) SelectToStartOfLine() bool {
152 if !v.Cursor.HasSelection() {
153 v.Cursor.OrigSelection[0] = v.Cursor.Loc
156 v.Cursor.SelectTo(v.Cursor.Loc)
160 // SelectToEndOfLine selects to the end of the current line
161 func (v *View) SelectToEndOfLine() bool {
162 if !v.Cursor.HasSelection() {
163 v.Cursor.OrigSelection[0] = v.Cursor.Loc
166 v.Cursor.SelectTo(v.Cursor.Loc)
170 // CursorStart moves the cursor to the start of the buffer
171 func (v *View) CursorStart() bool {
177 // CursorEnd moves the cursor to the end of the buffer
178 func (v *View) CursorEnd() bool {
179 v.Cursor.Loc = v.Buf.End()
183 // SelectToStart selects the text from the cursor to the start of the buffer
184 func (v *View) SelectToStart() bool {
185 if !v.Cursor.HasSelection() {
186 v.Cursor.OrigSelection[0] = v.Cursor.Loc
189 v.Cursor.SelectTo(v.Buf.Start())
193 // SelectToEnd selects the text from the cursor to the end of the buffer
194 func (v *View) SelectToEnd() bool {
195 if !v.Cursor.HasSelection() {
196 v.Cursor.OrigSelection[0] = v.Cursor.Loc
199 v.Cursor.SelectTo(v.Buf.End())
203 // InsertSpace inserts a space
204 func (v *View) InsertSpace() bool {
205 if v.Cursor.HasSelection() {
206 v.Cursor.DeleteSelection()
207 v.Cursor.ResetSelection()
209 v.Buf.Insert(v.Cursor.Loc, " ")
214 // InsertEnter inserts a newline plus possible some whitespace if autoindent is on
215 func (v *View) InsertEnter() bool {
217 if v.Cursor.HasSelection() {
218 v.Cursor.DeleteSelection()
219 v.Cursor.ResetSelection()
222 v.Buf.Insert(v.Cursor.Loc, "\n")
223 ws := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y))
226 if settings["autoindent"].(bool) {
227 v.Buf.Insert(v.Cursor.Loc, ws)
228 for i := 0; i < len(ws); i++ {
232 v.Cursor.LastVisualX = v.Cursor.GetVisualX()
236 // Backspace deletes the previous character
237 func (v *View) Backspace() bool {
238 // Delete a character
239 if v.Cursor.HasSelection() {
240 v.Cursor.DeleteSelection()
241 v.Cursor.ResetSelection()
242 } else if v.Cursor.Loc.GreaterThan(v.Buf.Start()) {
243 // We have to do something a bit hacky here because we want to
244 // delete the line by first moving left and then deleting backwards
245 // but the undo redo would place the cursor in the wrong place
246 // So instead we move left, save the position, move back, delete
247 // and restore the position
249 // If the user is using spaces instead of tabs and they are deleting
250 // whitespace at the start of the line, we should delete as if its a
251 // tab (tabSize number of spaces)
252 lineStart := v.Buf.Line(v.Cursor.Y)[:v.Cursor.X]
253 tabSize := int(settings["tabsize"].(float64))
254 if settings["tabstospaces"].(bool) && IsSpaces(lineStart) && len(lineStart) != 0 && len(lineStart)%tabSize == 0 {
256 v.Cursor.Loc = loc.Move(-tabSize, v.Buf)
257 cx, cy := v.Cursor.X, v.Cursor.Y
259 v.Buf.Remove(loc.Move(-tabSize, v.Buf), loc)
260 v.Cursor.X, v.Cursor.Y = cx, cy
263 cx, cy := v.Cursor.X, v.Cursor.Y
266 v.Buf.Remove(loc.Move(-1, v.Buf), loc)
267 v.Cursor.X, v.Cursor.Y = cx, cy
270 v.Cursor.LastVisualX = v.Cursor.GetVisualX()
274 // DeleteWordRight deletes the word to the right of the cursor
275 func (v *View) DeleteWordRight() bool {
277 if v.Cursor.HasSelection() {
278 v.Cursor.DeleteSelection()
279 v.Cursor.ResetSelection()
284 // DeleteWordLeft deletes the word to the left of the cursor
285 func (v *View) DeleteWordLeft() bool {
287 if v.Cursor.HasSelection() {
288 v.Cursor.DeleteSelection()
289 v.Cursor.ResetSelection()
294 // Delete deletes the next character
295 func (v *View) Delete() bool {
296 if v.Cursor.HasSelection() {
297 v.Cursor.DeleteSelection()
298 v.Cursor.ResetSelection()
301 if loc.LessThan(v.Buf.End()) {
302 v.Buf.Remove(loc, loc.Move(1, v.Buf))
308 // InsertTab inserts a tab or spaces
309 func (v *View) InsertTab() bool {
311 if v.Cursor.HasSelection() {
312 v.Cursor.DeleteSelection()
313 v.Cursor.ResetSelection()
315 if settings["tabstospaces"].(bool) {
316 tabSize := int(settings["tabsize"].(float64))
317 v.Buf.Insert(v.Cursor.Loc, Spaces(tabSize))
318 for i := 0; i < tabSize; i++ {
322 v.Buf.Insert(v.Cursor.Loc, "\t")
328 // Save the buffer to disk
329 func (v *View) Save() bool {
331 // We can't save the help text
334 // If this is an empty buffer, ask for a filename
335 if v.Buf.Path == "" {
336 filename, canceled := messenger.Prompt("Filename: ", "Save", NoCompletion)
338 v.Buf.Path = filename
339 v.Buf.Name = filename
346 if strings.HasSuffix(err.Error(), "permission denied") {
347 choice, _ := messenger.YesNoPrompt("Permission denied. Do you want to save this file using sudo? (y,n)")
349 err = v.Buf.SaveWithSudo()
351 messenger.Error(err.Error())
354 messenger.Message("Saved " + v.Buf.Path)
359 messenger.Error(err.Error())
362 messenger.Message("Saved " + v.Buf.Path)
367 // Find opens a prompt and searches forward for the input
368 func (v *View) Find() bool {
369 if v.Cursor.HasSelection() {
370 searchStart = ToCharPos(v.Cursor.CurSelection[1], v.Buf)
372 searchStart = ToCharPos(v.Cursor.Loc, v.Buf)
378 // FindNext searches forwards for the last used search term
379 func (v *View) FindNext() bool {
380 if v.Cursor.HasSelection() {
381 searchStart = ToCharPos(v.Cursor.CurSelection[1], v.Buf)
383 searchStart = ToCharPos(v.Cursor.Loc, v.Buf)
385 messenger.Message("Finding: " + lastSearch)
386 Search(lastSearch, v, true)
390 // FindPrevious searches backwards for the last used search term
391 func (v *View) FindPrevious() bool {
392 if v.Cursor.HasSelection() {
393 searchStart = ToCharPos(v.Cursor.CurSelection[0], v.Buf)
395 searchStart = ToCharPos(v.Cursor.Loc, v.Buf)
397 messenger.Message("Finding: " + lastSearch)
398 Search(lastSearch, v, false)
402 // Undo undoes the last action
403 func (v *View) Undo() bool {
405 messenger.Message("Undid action")
409 // Redo redoes the last action
410 func (v *View) Redo() bool {
412 messenger.Message("Redid action")
416 // Copy the selection to the system clipboard
417 func (v *View) Copy() bool {
418 if v.Cursor.HasSelection() {
419 clipboard.WriteAll(v.Cursor.GetSelection())
421 messenger.Message("Copied selection")
426 // CutLine cuts the current line to the clipboard
427 func (v *View) CutLine() bool {
428 v.Cursor.SelectLine()
429 if !v.Cursor.HasSelection() {
432 if v.freshClip == true {
433 if v.Cursor.HasSelection() {
434 if clip, err := clipboard.ReadAll(); err != nil {
437 clipboard.WriteAll(clip + v.Cursor.GetSelection())
440 } else if time.Since(v.lastCutTime)/time.Second > 10*time.Second || v.freshClip == false {
444 v.lastCutTime = time.Now()
445 v.Cursor.DeleteSelection()
446 v.Cursor.ResetSelection()
447 messenger.Message("Cut line")
451 // Cut the selection to the system clipboard
452 func (v *View) Cut() bool {
453 if v.Cursor.HasSelection() {
454 clipboard.WriteAll(v.Cursor.GetSelection())
455 v.Cursor.DeleteSelection()
456 v.Cursor.ResetSelection()
458 messenger.Message("Cut selection")
463 // DuplicateLine duplicates the current line
464 func (v *View) DuplicateLine() bool {
466 v.Buf.Insert(v.Cursor.Loc, "\n"+v.Buf.Line(v.Cursor.Y))
468 messenger.Message("Duplicated line")
472 // DeleteLine deletes the current line
473 func (v *View) DeleteLine() bool {
474 v.Cursor.SelectLine()
475 if !v.Cursor.HasSelection() {
478 v.Cursor.DeleteSelection()
479 v.Cursor.ResetSelection()
480 messenger.Message("Deleted line")
484 // Paste whatever is in the system clipboard into the buffer
485 // Delete and paste if the user has a selection
486 func (v *View) Paste() bool {
487 if v.Cursor.HasSelection() {
488 v.Cursor.DeleteSelection()
489 v.Cursor.ResetSelection()
491 clip, _ := clipboard.ReadAll()
492 v.Buf.Insert(v.Cursor.Loc, clip)
493 v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf)
495 messenger.Message("Pasted clipboard")
499 // SelectAll selects the entire buffer
500 func (v *View) SelectAll() bool {
501 v.Cursor.CurSelection[0] = v.Buf.Start()
502 v.Cursor.CurSelection[1] = v.Buf.End()
503 // Put the cursor at the beginning
509 // OpenFile opens a new file in the buffer
510 func (v *View) OpenFile() bool {
511 if v.CanClose("Continue? (yes, no, save) ") {
512 filename, canceled := messenger.Prompt("File to open: ", "Open", FileCompletion)
516 home, _ := homedir.Dir()
517 filename = strings.Replace(filename, "~", home, 1)
518 file, err := ioutil.ReadFile(filename)
522 // File does not exist -- create an empty buffer with that name
523 buf = NewBuffer([]byte{}, filename)
525 buf = NewBuffer(file, filename)
533 // Start moves the viewport to the start of the buffer
534 func (v *View) Start() bool {
539 // End moves the viewport to the end of the buffer
540 func (v *View) End() bool {
541 if v.height > v.Buf.NumLines {
544 v.Topline = v.Buf.NumLines - v.height
549 // PageUp scrolls the view up a page
550 func (v *View) PageUp() bool {
551 if v.Topline > v.height {
559 // PageDown scrolls the view down a page
560 func (v *View) PageDown() bool {
561 if v.Buf.NumLines-(v.Topline+v.height) > v.height {
562 v.ScrollDown(v.height)
563 } else if v.Buf.NumLines >= v.height {
564 v.Topline = v.Buf.NumLines - v.height
569 // CursorPageUp places the cursor a page up
570 func (v *View) CursorPageUp() bool {
571 if v.Cursor.HasSelection() {
572 v.Cursor.Loc = v.Cursor.CurSelection[0]
573 v.Cursor.ResetSelection()
575 v.Cursor.UpN(v.height)
579 // CursorPageDown places the cursor a page up
580 func (v *View) CursorPageDown() bool {
581 if v.Cursor.HasSelection() {
582 v.Cursor.Loc = v.Cursor.CurSelection[1]
583 v.Cursor.ResetSelection()
585 v.Cursor.DownN(v.height)
589 // HalfPageUp scrolls the view up half a page
590 func (v *View) HalfPageUp() bool {
591 if v.Topline > v.height/2 {
592 v.ScrollUp(v.height / 2)
599 // HalfPageDown scrolls the view down half a page
600 func (v *View) HalfPageDown() bool {
601 if v.Buf.NumLines-(v.Topline+v.height) > v.height/2 {
602 v.ScrollDown(v.height / 2)
604 if v.Buf.NumLines >= v.height {
605 v.Topline = v.Buf.NumLines - v.height
611 // ToggleRuler turns line numbers off and on
612 func (v *View) ToggleRuler() bool {
613 if settings["ruler"] == false {
614 settings["ruler"] = true
615 messenger.Message("Enabled ruler")
617 settings["ruler"] = false
618 messenger.Message("Disabled ruler")
623 // JumpLine jumps to a line and moves the view accordingly.
624 func (v *View) JumpLine() bool {
625 // Prompt for line number
626 linestring, canceled := messenger.Prompt("Jump to line # ", "LineNumber", NoCompletion)
630 lineint, err := strconv.Atoi(linestring)
631 lineint = lineint - 1 // fix offset
633 messenger.Error(err) // return errors
636 // Move cursor and view if possible.
637 if lineint < v.Buf.NumLines && lineint >= 0 {
642 messenger.Error("Only ", v.Buf.NumLines, " lines to jump")
646 // ClearStatus clears the messenger bar
647 func (v *View) ClearStatus() bool {
648 messenger.Message("")
652 // ToggleHelp toggles the help screen
653 func (v *View) ToggleHelp() bool {
656 helpBuffer := NewBuffer([]byte(helpTxt), "help.md")
657 helpBuffer.Name = "Help"
659 v.OpenBuffer(helpBuffer)
661 v.OpenBuffer(v.lastBuffer)
667 // ShellMode opens a terminal to run a shell command
668 func (v *View) ShellMode() bool {
669 input, canceled := messenger.Prompt("$ ", "Shell", NoCompletion)
671 // The true here is for openTerm to make the command interactive
672 HandleShellCommand(input, true)
677 // CommandMode lets the user enter a command
678 func (v *View) CommandMode() bool {
679 input, canceled := messenger.Prompt("> ", "Command", NoCompletion)
686 // Quit quits the editor
687 // This behavior needs to be changed and should really only quit the editor if this
689 // However, since micro only supports one view for now, it doesn't really matter
690 func (v *View) Quit() bool {
692 return v.ToggleHelp()
694 // Make sure not to quit if there are unsaved changes
695 if v.CanClose("Quit anyway? (yes, no, save) ") {
697 if len(tabs[curTab].views) > 1 {
699 if v.splitChild != nil {
701 view.splitParent = v.splitParent
702 } else if v.splitParent != nil {
704 v.splitParent.splitChild = nil
706 view.x, view.y = view.splitOrigPos[0], view.splitOrigPos[1]
707 view.widthPercent, view.heightPercent = view.splitOrigDimensions[0], view.splitOrigDimensions[1]
708 view.Resize(screen.Size())
709 if settings["syntax"].(bool) {
710 view.matches = Match(view)
712 tabs[curTab].views = tabs[curTab].views[:v.Num+copy(tabs[curTab].views[v.Num:], tabs[curTab].views[v.Num+1:])]
713 for i, v := range tabs[curTab].views {
716 tabs[curTab].curView = view.Num
717 } else if len(tabs) > 1 {
718 if len(tabs[v.TabNum].views) == 1 {
719 tabs = tabs[:v.TabNum+copy(tabs[v.TabNum:], tabs[v.TabNum+1:])]
720 for i, t := range tabs {
723 if curTab >= len(tabs) {
727 CurView().Resize(screen.Size())
728 CurView().matches = Match(CurView())
739 // AddTab adds a new tab with an empty buffer
740 func (v *View) AddTab() bool {
741 tab := NewTabFromView(NewView(NewBuffer([]byte{}, "")))
742 tab.SetNum(len(tabs))
743 tabs = append(tabs, tab)
746 for _, t := range tabs {
747 for _, v := range t.views {
748 v.Resize(screen.Size())
755 // PreviousTab switches to the previous tab in the tab list
756 func (v *View) PreviousTab() bool {
759 } else if curTab == 0 {
760 curTab = len(tabs) - 1
765 // NextTab switches to the next tab in the tab list
766 func (v *View) NextTab() bool {
767 if curTab < len(tabs)-1 {
769 } else if curTab == len(tabs)-1 {
775 // Changes the view to the next split
776 func (v *View) NextSplit() bool {
778 if tab.curView < len(tab.views)-1 {
786 // Changes the view to the previous split
787 func (v *View) PreviousSplit() bool {
792 tab.curView = len(tab.views) - 1