4 "github.com/atotto/clipboard"
5 "github.com/gdamore/tcell"
12 // The View struct stores information about a view into a buffer.
13 // It has a stores information about the cursor, and the viewport
14 // that the user sees the buffer from.
18 // The topmost line, used for vertical scrolling
20 // The leftmost column, used for horizontal scrolling
23 // Percentage of the terminal window that this view takes up (from 0 to 100)
27 // Actual with and height
31 // How much to offset because of line numbers
34 // The eventhandler for undo/redo
42 // Since tcell doesn't differentiate between a mouse release event
43 // and a mouse move event with no keys pressed, we need to keep
44 // track of whether or not the mouse was pressed (or not released) last event to determine
45 // mouse release events
48 // This stores when the last click was
49 // This is useful for detecting double and triple clicks
50 lastClickTime time.Time
52 // Was the last mouse event actually a double click?
53 // Useful for detecting triple clicks -- if a double click is detected
54 // but the last mouse event was actually a double click, it's a triple click
56 // Same here, just to keep track for mouse move events
59 // Syntax highlighting matches
61 // The matches from the last frame
62 lastMatches SyntaxMatches
64 // This is the range of lines that should have their syntax highlighting updated
68 // NewView returns a new fullscreen view
69 func NewView(buf *Buffer) *View {
70 return NewViewWidthHeight(buf, 100, 100)
73 // NewViewWidthHeight returns a new view with the specified width and height percentages
74 // Note that w and h are percentages not actual values
75 func NewViewWidthHeight(buf *Buffer, w, h int) *View {
82 v.Resize(screen.Size())
85 // Put the cursor at the first spot
91 v.cursor.ResetSelection()
93 v.eh = NewEventHandler(v)
99 // Update the syntax highlighting for the entire buffer at the start
100 v.UpdateLines(v.topline, v.topline+v.height)
103 // Set mouseReleased to true because we assume the mouse is not being pressed when
104 // the editor is opened
105 v.mouseReleased = true
106 v.lastClickTime = time.Time{}
111 // UpdateLines sets the values for v.updateLines
112 func (v *View) UpdateLines(start, end int) {
113 v.updateLines[0] = start
114 v.updateLines[1] = end + 1
117 // Resize recalculates the actual width and height of the view from the width and height
119 // This is usually called when the window is resized, or when a split has been added and
120 // the percentages have changed
121 func (v *View) Resize(w, h int) {
122 // Always include 1 line for the command line at the bottom
124 v.width = int(float32(w) * float32(v.widthPercent) / 100)
125 // We subtract 1 for the statusline
126 v.height = int(float32(h)*float32(v.heightPercent)/100) - 1
129 // ScrollUp scrolls the view up n lines (if possible)
130 func (v *View) ScrollUp(n int) {
131 // Try to scroll by n but if it would overflow, scroll by 1
132 if v.topline-n >= 0 {
134 } else if v.topline > 0 {
139 // ScrollDown scrolls the view down n lines (if possible)
140 func (v *View) ScrollDown(n int) {
141 // Try to scroll by n but if it would overflow, scroll by 1
142 if v.topline+n <= len(v.buf.lines)-v.height {
144 } else if v.topline < len(v.buf.lines)-v.height {
149 // PageUp scrolls the view up a page
150 func (v *View) PageUp() {
151 if v.topline > v.height {
158 // PageDown scrolls the view down a page
159 func (v *View) PageDown() {
160 if len(v.buf.lines)-(v.topline+v.height) > v.height {
161 v.ScrollDown(v.height)
163 if len(v.buf.lines) >= v.height {
164 v.topline = len(v.buf.lines) - v.height
169 // HalfPageUp scrolls the view up half a page
170 func (v *View) HalfPageUp() {
171 if v.topline > v.height/2 {
172 v.ScrollUp(v.height / 2)
178 // HalfPageDown scrolls the view down half a page
179 func (v *View) HalfPageDown() {
180 if len(v.buf.lines)-(v.topline+v.height) > v.height/2 {
181 v.ScrollDown(v.height / 2)
183 if len(v.buf.lines) >= v.height {
184 v.topline = len(v.buf.lines) - v.height
189 // CanClose returns whether or not the view can be closed
190 // If there are unsaved changes, the user will be asked if the view can be closed
191 // causing them to lose the unsaved changes
192 // The message is what to print after saying "You have unsaved changes. "
193 func (v *View) CanClose(msg string) bool {
195 quit, canceled := messenger.Prompt("You have unsaved changes. " + msg)
197 if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
199 } else if strings.ToLower(quit) == "save" || strings.ToLower(quit) == "s" {
210 // Save the buffer to disk
211 func (v *View) Save() {
212 // If this is an empty buffer, ask for a filename
213 if v.buf.path == "" {
214 filename, canceled := messenger.Prompt("Filename: ")
216 v.buf.path = filename
217 v.buf.name = filename
224 messenger.Error(err.Error())
226 messenger.Message("Saved " + v.buf.path)
230 // Copy the selection to the system clipboard
231 func (v *View) Copy() {
232 if v.cursor.HasSelection() {
233 if !clipboard.Unsupported {
234 clipboard.WriteAll(v.cursor.GetSelection())
236 messenger.Error("Clipboard is not supported on your system")
241 // Cut the selection to the system clipboard
242 func (v *View) Cut() {
243 if v.cursor.HasSelection() {
244 if !clipboard.Unsupported {
245 clipboard.WriteAll(v.cursor.GetSelection())
246 v.cursor.DeleteSelection()
247 v.cursor.ResetSelection()
249 messenger.Error("Clipboard is not supported on your system")
254 // Paste whatever is in the system clipboard into the buffer
255 // Delete and paste if the user has a selection
256 func (v *View) Paste() {
257 if !clipboard.Unsupported {
258 if v.cursor.HasSelection() {
259 v.cursor.DeleteSelection()
260 v.cursor.ResetSelection()
262 clip, _ := clipboard.ReadAll()
263 v.eh.Insert(v.cursor.Loc(), clip)
264 v.cursor.SetLoc(v.cursor.Loc() + Count(clip))
266 messenger.Error("Clipboard is not supported on your system")
270 // SelectAll selects the entire buffer
271 func (v *View) SelectAll() {
272 v.cursor.curSelection[1] = 0
273 v.cursor.curSelection[0] = v.buf.Len()
274 // Put the cursor at the beginning
279 // OpenBuffer opens a new buffer in this view.
280 // This resets the topline, event handler and cursor.
281 func (v *View) OpenBuffer(buf *Buffer) {
284 // Put the cursor at the first spot
290 v.cursor.ResetSelection()
292 v.eh = NewEventHandler(v)
296 // Set mouseReleased to true because we assume the mouse is not being pressed when
297 // the editor is opened
298 v.mouseReleased = true
299 v.lastClickTime = time.Time{}
302 // OpenFile opens a new file in the current view
303 // It makes sure that the current buffer can be closed first (unsaved changes)
304 func (v *View) OpenFile() {
305 if v.CanClose("Continue? (yes, no, save) ") {
306 filename, canceled := messenger.Prompt("File to open: ")
310 file, err := ioutil.ReadFile(filename)
313 messenger.Error(err.Error())
316 v.buf = NewBuffer(string(file), filename)
320 // Relocate moves the view window so that the cursor is in view
321 // This is useful if the user has scrolled far away, and then starts typing
322 func (v *View) Relocate() bool {
329 if cy > v.topline+v.height-1 {
330 v.topline = cy - v.height + 1
334 cx := v.cursor.GetVisualX()
339 if cx+v.lineNumOffset+1 > v.leftCol+v.width {
340 v.leftCol = cx - v.width + v.lineNumOffset + 1
346 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
348 func (v *View) MoveToMouseClick(x, y int) {
349 if y-v.topline > v.height-1 {
351 y = v.height + v.topline - 1
353 if y >= len(v.buf.lines) {
354 y = len(v.buf.lines) - 1
363 x = v.cursor.GetCharPosInLine(y, x)
364 if x > Count(v.buf.lines[y]) {
365 x = Count(v.buf.lines[y])
369 v.cursor.lastVisualX = v.cursor.GetVisualX()
372 // HandleEvent handles an event passed by the main loop
373 func (v *View) HandleEvent(event tcell.Event) {
374 // This bool determines whether the view is relocated at the end of the function
375 // By default it's true because most events should cause a relocate
378 // By default we don't update and syntax highlighting
380 switch e := event.(type) {
381 case *tcell.EventResize:
384 case *tcell.EventKey:
388 v.cursor.ResetSelection()
392 v.cursor.ResetSelection()
396 v.cursor.ResetSelection()
400 v.cursor.ResetSelection()
404 if v.cursor.HasSelection() {
405 v.cursor.DeleteSelection()
406 v.cursor.ResetSelection()
408 v.eh.Insert(v.cursor.Loc(), "\n")
410 // Rehighlight the entire buffer
411 v.UpdateLines(v.topline, v.topline+v.height)
412 v.cursor.lastVisualX = v.cursor.GetVisualX()
413 // v.UpdateLines(v.cursor.y-1, v.cursor.y)
416 if v.cursor.HasSelection() {
417 v.cursor.DeleteSelection()
418 v.cursor.ResetSelection()
420 v.eh.Insert(v.cursor.Loc(), " ")
422 v.UpdateLines(v.cursor.y, v.cursor.y)
423 case tcell.KeyBackspace2, tcell.KeyBackspace:
424 // Delete a character
425 if v.cursor.HasSelection() {
426 v.cursor.DeleteSelection()
427 v.cursor.ResetSelection()
428 // Rehighlight the entire buffer
429 v.UpdateLines(v.topline, v.topline+v.height)
430 } else if v.cursor.Loc() > 0 {
431 // We have to do something a bit hacky here because we want to
432 // delete the line by first moving left and then deleting backwards
433 // but the undo redo would place the cursor in the wrong place
434 // So instead we move left, save the position, move back, delete
435 // and restore the position
437 cx, cy := v.cursor.x, v.cursor.y
439 loc := v.cursor.Loc()
440 v.eh.Remove(loc-1, loc)
441 v.cursor.x, v.cursor.y = cx, cy
442 // Rehighlight the entire buffer
443 v.UpdateLines(v.topline, v.topline+v.height)
444 // v.UpdateLines(v.cursor.y, v.cursor.y+1)
446 v.cursor.lastVisualX = v.cursor.GetVisualX()
449 if v.cursor.HasSelection() {
450 v.cursor.DeleteSelection()
451 v.cursor.ResetSelection()
453 if settings.TabsToSpaces {
454 v.eh.Insert(v.cursor.Loc(), Spaces(settings.TabSize))
455 for i := 0; i < settings.TabSize; i++ {
459 v.eh.Insert(v.cursor.Loc(), "\t")
462 v.UpdateLines(v.cursor.y, v.cursor.y)
466 if v.cursor.HasSelection() {
467 searchStart = v.cursor.curSelection[1]
469 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
473 if v.cursor.HasSelection() {
474 searchStart = v.cursor.curSelection[1]
476 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
478 messenger.Message("Find: " + lastSearch)
479 Search(lastSearch, v, true)
481 if v.cursor.HasSelection() {
482 searchStart = v.cursor.curSelection[0]
484 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
486 messenger.Message("Find: " + lastSearch)
487 Search(lastSearch, v, false)
490 // Rehighlight the entire buffer
491 v.UpdateLines(v.topline, v.topline+v.height)
494 // Rehighlight the entire buffer
495 v.UpdateLines(v.topline, v.topline+v.height)
498 // Rehighlight the entire buffer
499 v.UpdateLines(v.topline, v.topline+v.height)
502 // Rehighlight the entire buffer
503 v.UpdateLines(v.topline, v.topline+v.height)
506 // Rehighlight the entire buffer
507 v.UpdateLines(v.topline, v.topline+v.height)
512 // Rehighlight the entire buffer
513 v.UpdateLines(v.topline, v.topline+v.height)
518 if v.height > len(v.buf.lines) {
521 v.topline = len(v.buf.lines) - v.height
537 // Insert a character
538 if v.cursor.HasSelection() {
539 v.cursor.DeleteSelection()
540 v.cursor.ResetSelection()
541 // Rehighlight the entire buffer
542 v.UpdateLines(v.topline, v.topline+v.height)
544 v.UpdateLines(v.cursor.y, v.cursor.y)
546 v.eh.Insert(v.cursor.Loc(), string(e.Rune()))
549 case *tcell.EventMouse:
551 x -= v.lineNumOffset - v.leftCol
553 // Position always seems to be off by one
557 button := e.Buttons()
562 origX, origY := v.cursor.x, v.cursor.y
563 v.MoveToMouseClick(x, y)
566 if (time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold) &&
567 (origX == v.cursor.x && origY == v.cursor.y) {
570 v.lastClickTime = time.Now()
573 v.doubleClick = false
575 v.cursor.SelectLine()
578 v.lastClickTime = time.Now()
581 v.tripleClick = false
583 v.cursor.SelectWord()
586 v.doubleClick = false
587 v.tripleClick = false
588 v.lastClickTime = time.Now()
590 loc := v.cursor.Loc()
591 v.cursor.curSelection[0] = loc
592 v.cursor.curSelection[1] = loc
596 v.cursor.AddLineToSelection()
597 } else if v.doubleClick {
598 v.cursor.AddWordToSelection()
600 v.cursor.curSelection[1] = v.cursor.Loc()
603 v.mouseReleased = false
604 case tcell.ButtonNone:
605 // Mouse event with no click
606 if !v.mouseReleased {
607 // Mouse was just released
609 // Relocating here isn't really necessary because the cursor will
610 // be in the right place from the last mouse event
611 // However, if we are running in a terminal that doesn't support mouse motion
612 // events, this still allows the user to make selections, except only after they
615 if !v.doubleClick && !v.tripleClick {
616 v.MoveToMouseClick(x, y)
617 v.cursor.curSelection[1] = v.cursor.Loc()
619 v.mouseReleased = true
621 // We don't want to relocate because otherwise the view will be relocated
622 // every time the user moves the cursor
625 // Scroll up two lines
627 // We don't want to relocate if the user is scrolling
629 // Rehighlight the entire buffer
630 v.UpdateLines(v.topline, v.topline+v.height)
631 case tcell.WheelDown:
632 // Scroll down two lines
634 // We don't want to relocate if the user is scrolling
636 // Rehighlight the entire buffer
637 v.UpdateLines(v.topline, v.topline+v.height)
649 // DisplayView renders the view to the screen
650 func (v *View) DisplayView() {
651 // matches := make(SyntaxMatches, len(v.buf.lines))
653 // viewStart := v.topline
654 // viewEnd := v.topline + v.height
655 // if viewEnd > len(v.buf.lines) {
656 // viewEnd = len(v.buf.lines)
659 // lines := v.buf.lines[viewStart:viewEnd]
660 // for i, line := range lines {
661 // matches[i] = make([]tcell.Style, len(line))
664 // The character number of the character in the top left of the screen
666 charNum := ToCharPos(0, v.topline, v.buf)
668 // Convert the length of buffer to a string, and get the length of the string
669 // We are going to have to offset by that amount
670 maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
671 // + 1 for the little space after the line number
672 v.lineNumOffset = maxLineLength + 1
674 var highlightStyle tcell.Style
676 for lineN := 0; lineN < v.height; lineN++ {
678 // If the buffer is smaller than the view height
679 // and we went too far, break
680 if lineN+v.topline >= len(v.buf.lines) {
683 line := v.buf.lines[lineN+v.topline]
685 // Write the line number
686 lineNumStyle := defStyle
687 if style, ok := colorscheme["line-number"]; ok {
690 // Write the spaces before the line number if necessary
691 lineNum := strconv.Itoa(lineN + v.topline + 1)
692 for i := 0; i < maxLineLength-len(lineNum); i++ {
693 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
696 // Write the actual line number
697 for _, ch := range lineNum {
698 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
701 // Write the extra space
702 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
707 runes := []rune(line)
708 for colN := v.leftCol; colN < v.leftCol+v.width; colN++ {
709 if colN >= len(runes) {
713 var lineStyle tcell.Style
714 // Does the current character need to be syntax highlighted?
716 // if lineN >= v.updateLines[0] && lineN < v.updateLines[1] {
718 highlightStyle = v.matches[lineN][colN]
720 // } else if lineN < len(v.lastMatches) && colN < len(v.lastMatches[lineN]) {
721 // highlightStyle = v.lastMatches[lineN][colN]
723 // highlightStyle = defStyle
726 if v.cursor.HasSelection() &&
727 (charNum >= v.cursor.curSelection[0] && charNum < v.cursor.curSelection[1] ||
728 charNum < v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
730 lineStyle = defStyle.Reverse(true)
732 if style, ok := colorscheme["selection"]; ok {
736 lineStyle = highlightStyle
738 // matches[lineN][colN] = highlightStyle
741 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
742 tabSize := settings.TabSize
743 for i := 0; i < tabSize-1; i++ {
745 if x-v.leftCol+tabchars >= v.lineNumOffset {
746 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
750 if x-v.leftCol+tabchars >= v.lineNumOffset {
751 screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
757 // Here we are at a newline
759 // The newline may be selected, in which case we should draw the selection style
760 // with a space to represent it
761 if v.cursor.HasSelection() &&
762 (charNum >= v.cursor.curSelection[0] && charNum < v.cursor.curSelection[1] ||
763 charNum < v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
765 selectStyle := defStyle.Reverse(true)
767 if style, ok := colorscheme["selection"]; ok {
770 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
775 // v.lastMatches = matches
778 // Display renders the view, the cursor, and statusline
779 func (v *View) Display() {