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" {
207 // Save the buffer to disk
208 func (v *View) Save() {
209 // If this is an empty buffer, ask for a filename
210 if v.buf.path == "" {
211 filename, canceled := messenger.Prompt("Filename: ")
213 v.buf.path = filename
214 v.buf.name = filename
221 messenger.Error(err.Error())
223 messenger.Message("Saved " + v.buf.path)
227 // Copy the selection to the system clipboard
228 func (v *View) Copy() {
229 if v.cursor.HasSelection() {
230 if !clipboard.Unsupported {
231 clipboard.WriteAll(v.cursor.GetSelection())
233 messenger.Error("Clipboard is not supported on your system")
238 // Cut the selection to the system clipboard
239 func (v *View) Cut() {
240 if v.cursor.HasSelection() {
241 if !clipboard.Unsupported {
242 clipboard.WriteAll(v.cursor.GetSelection())
243 v.cursor.DeleteSelection()
244 v.cursor.ResetSelection()
246 messenger.Error("Clipboard is not supported on your system")
251 // Paste whatever is in the system clipboard into the buffer
252 // Delete and paste if the user has a selection
253 func (v *View) Paste() {
254 if !clipboard.Unsupported {
255 if v.cursor.HasSelection() {
256 v.cursor.DeleteSelection()
257 v.cursor.ResetSelection()
259 clip, _ := clipboard.ReadAll()
260 v.eh.Insert(v.cursor.Loc(), clip)
261 v.cursor.SetLoc(v.cursor.Loc() + Count(clip))
263 messenger.Error("Clipboard is not supported on your system")
267 // SelectAll selects the entire buffer
268 func (v *View) SelectAll() {
269 v.cursor.curSelection[1] = 0
270 v.cursor.curSelection[0] = v.buf.Len()
271 // Put the cursor at the beginning
276 // OpenFile opens a new file in the current view
277 // It makes sure that the current buffer can be closed first (unsaved changes)
278 func (v *View) OpenFile() {
279 if v.CanClose("Continue? ") {
280 filename, canceled := messenger.Prompt("File to open: ")
284 file, err := ioutil.ReadFile(filename)
287 messenger.Error(err.Error())
290 v.buf = NewBuffer(string(file), filename)
294 // Relocate moves the view window so that the cursor is in view
295 // This is useful if the user has scrolled far away, and then starts typing
296 func (v *View) Relocate() bool {
303 if cy > v.topline+v.height-1 {
304 v.topline = cy - v.height + 1
308 cx := v.cursor.GetVisualX()
313 if cx+v.lineNumOffset+1 > v.leftCol+v.width {
314 v.leftCol = cx - v.width + v.lineNumOffset + 1
320 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
322 func (v *View) MoveToMouseClick(x, y int) {
323 if y-v.topline > v.height-1 {
325 y = v.height + v.topline - 1
327 if y >= len(v.buf.lines) {
328 y = len(v.buf.lines) - 1
334 x = v.cursor.GetCharPosInLine(y, x)
335 if x > Count(v.buf.lines[y]) {
336 x = Count(v.buf.lines[y])
340 v.cursor.lastVisualX = v.cursor.GetVisualX()
343 // HandleEvent handles an event passed by the main loop
344 func (v *View) HandleEvent(event tcell.Event) {
345 // This bool determines whether the view is relocated at the end of the function
346 // By default it's true because most events should cause a relocate
349 // By default we don't update and syntax highlighting
351 switch e := event.(type) {
352 case *tcell.EventResize:
355 case *tcell.EventKey:
359 v.cursor.ResetSelection()
363 v.cursor.ResetSelection()
367 v.cursor.ResetSelection()
371 v.cursor.ResetSelection()
375 if v.cursor.HasSelection() {
376 v.cursor.DeleteSelection()
377 v.cursor.ResetSelection()
379 v.eh.Insert(v.cursor.Loc(), "\n")
381 // Rehighlight the entire buffer
382 v.UpdateLines(v.topline, v.topline+v.height)
383 v.cursor.lastVisualX = v.cursor.GetVisualX()
384 // v.UpdateLines(v.cursor.y-1, v.cursor.y)
387 if v.cursor.HasSelection() {
388 v.cursor.DeleteSelection()
389 v.cursor.ResetSelection()
391 v.eh.Insert(v.cursor.Loc(), " ")
393 v.UpdateLines(v.cursor.y, v.cursor.y)
394 case tcell.KeyBackspace2:
395 // Delete a character
396 if v.cursor.HasSelection() {
397 v.cursor.DeleteSelection()
398 v.cursor.ResetSelection()
399 // Rehighlight the entire buffer
400 v.UpdateLines(v.topline, v.topline+v.height)
401 } else if v.cursor.Loc() > 0 {
402 // We have to do something a bit hacky here because we want to
403 // delete the line by first moving left and then deleting backwards
404 // but the undo redo would place the cursor in the wrong place
405 // So instead we move left, save the position, move back, delete
406 // and restore the position
408 cx, cy := v.cursor.x, v.cursor.y
410 loc := v.cursor.Loc()
411 v.eh.Remove(loc-1, loc)
412 v.cursor.x, v.cursor.y = cx, cy
413 // Rehighlight the entire buffer
414 v.UpdateLines(v.topline, v.topline+v.height)
415 // v.UpdateLines(v.cursor.y, v.cursor.y+1)
417 v.cursor.lastVisualX = v.cursor.GetVisualX()
420 if v.cursor.HasSelection() {
421 v.cursor.DeleteSelection()
422 v.cursor.ResetSelection()
424 v.eh.Insert(v.cursor.Loc(), "\t")
426 v.UpdateLines(v.cursor.y, v.cursor.y)
430 if v.cursor.HasSelection() {
431 searchStart = v.cursor.curSelection[1]
433 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
437 if v.cursor.HasSelection() {
438 searchStart = v.cursor.curSelection[1]
440 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
442 messenger.Message("Find: " + lastSearch)
443 Search(lastSearch, v, true)
445 if v.cursor.HasSelection() {
446 searchStart = v.cursor.curSelection[0]
448 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
450 messenger.Message("Find: " + lastSearch)
451 Search(lastSearch, v, false)
454 // Rehighlight the entire buffer
455 v.UpdateLines(v.topline, v.topline+v.height)
458 // Rehighlight the entire buffer
459 v.UpdateLines(v.topline, v.topline+v.height)
462 // Rehighlight the entire buffer
463 v.UpdateLines(v.topline, v.topline+v.height)
466 // Rehighlight the entire buffer
467 v.UpdateLines(v.topline, v.topline+v.height)
470 // Rehighlight the entire buffer
471 v.UpdateLines(v.topline, v.topline+v.height)
476 // Rehighlight the entire buffer
477 v.UpdateLines(v.topline, v.topline+v.height)
491 // Insert a character
492 if v.cursor.HasSelection() {
493 v.cursor.DeleteSelection()
494 v.cursor.ResetSelection()
495 // Rehighlight the entire buffer
496 v.UpdateLines(v.topline, v.topline+v.height)
498 v.UpdateLines(v.cursor.y, v.cursor.y)
500 v.eh.Insert(v.cursor.Loc(), string(e.Rune()))
503 case *tcell.EventMouse:
505 x -= v.lineNumOffset - v.leftCol
507 // Position always seems to be off by one
511 button := e.Buttons()
516 origX, origY := v.cursor.x, v.cursor.y
517 v.MoveToMouseClick(x, y)
520 if (time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold) &&
521 (origX == v.cursor.x && origY == v.cursor.y) {
524 v.lastClickTime = time.Now()
527 v.doubleClick = false
529 v.cursor.SelectLine()
532 v.lastClickTime = time.Now()
535 v.tripleClick = false
537 v.cursor.SelectWord()
540 v.doubleClick = false
541 v.tripleClick = false
542 v.lastClickTime = time.Now()
544 loc := v.cursor.Loc()
545 v.cursor.curSelection[0] = loc
546 v.cursor.curSelection[1] = loc
550 v.cursor.AddLineToSelection()
551 } else if v.doubleClick {
552 v.cursor.AddWordToSelection()
554 v.cursor.curSelection[1] = v.cursor.Loc()
557 v.mouseReleased = false
558 case tcell.ButtonNone:
559 // Mouse event with no click
560 if !v.mouseReleased {
561 // Mouse was just released
563 // Relocating here isn't really necessary because the cursor will
564 // be in the right place from the last mouse event
565 // However, if we are running in a terminal that doesn't support mouse motion
566 // events, this still allows the user to make selections, except only after they
569 if !v.doubleClick && !v.tripleClick {
570 v.MoveToMouseClick(x, y)
571 v.cursor.curSelection[1] = v.cursor.Loc()
573 v.mouseReleased = true
575 // We don't want to relocate because otherwise the view will be relocated
576 // every time the user moves the cursor
579 // Scroll up two lines
581 // We don't want to relocate if the user is scrolling
583 // Rehighlight the entire buffer
584 v.UpdateLines(v.topline, v.topline+v.height)
585 case tcell.WheelDown:
586 // Scroll down two lines
588 // We don't want to relocate if the user is scrolling
590 // Rehighlight the entire buffer
591 v.UpdateLines(v.topline, v.topline+v.height)
603 // DisplayView renders the view to the screen
604 func (v *View) DisplayView() {
605 // matches := make(SyntaxMatches, len(v.buf.lines))
607 // viewStart := v.topline
608 // viewEnd := v.topline + v.height
609 // if viewEnd > len(v.buf.lines) {
610 // viewEnd = len(v.buf.lines)
613 // lines := v.buf.lines[viewStart:viewEnd]
614 // for i, line := range lines {
615 // matches[i] = make([]tcell.Style, len(line))
618 // The character number of the character in the top left of the screen
620 charNum := ToCharPos(0, v.topline, v.buf)
622 // Convert the length of buffer to a string, and get the length of the string
623 // We are going to have to offset by that amount
624 maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
625 // + 1 for the little space after the line number
626 v.lineNumOffset = maxLineLength + 1
628 var highlightStyle tcell.Style
630 for lineN := 0; lineN < v.height; lineN++ {
632 // If the buffer is smaller than the view height
633 // and we went too far, break
634 if lineN+v.topline >= len(v.buf.lines) {
637 line := v.buf.lines[lineN+v.topline]
639 // Write the line number
640 lineNumStyle := defStyle
641 if style, ok := colorscheme["line-number"]; ok {
644 // Write the spaces before the line number if necessary
645 lineNum := strconv.Itoa(lineN + v.topline + 1)
646 for i := 0; i < maxLineLength-len(lineNum); i++ {
647 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
650 // Write the actual line number
651 for _, ch := range lineNum {
652 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
655 // Write the extra space
656 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
661 runes := []rune(line)
662 for colN := v.leftCol; colN < v.leftCol+v.width; colN++ {
663 if colN >= len(runes) {
667 var lineStyle tcell.Style
668 // Does the current character need to be syntax highlighted?
670 // if lineN >= v.updateLines[0] && lineN < v.updateLines[1] {
672 highlightStyle = v.matches[lineN][colN]
674 // } else if lineN < len(v.lastMatches) && colN < len(v.lastMatches[lineN]) {
675 // highlightStyle = v.lastMatches[lineN][colN]
677 // highlightStyle = defStyle
680 if v.cursor.HasSelection() &&
681 (charNum >= v.cursor.curSelection[0] && charNum < v.cursor.curSelection[1] ||
682 charNum < v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
684 lineStyle = defStyle.Reverse(true)
686 if style, ok := colorscheme["selection"]; ok {
690 lineStyle = highlightStyle
692 // matches[lineN][colN] = highlightStyle
695 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
696 tabSize := settings.TabSize
697 for i := 0; i < tabSize-1; i++ {
699 if x-v.leftCol+tabchars >= v.lineNumOffset {
700 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
704 if x-v.leftCol+tabchars >= v.lineNumOffset {
705 screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
711 // Here we are at a newline
713 // The newline may be selected, in which case we should draw the selection style
714 // with a space to represent it
715 if v.cursor.HasSelection() &&
716 (charNum >= v.cursor.curSelection[0] && charNum < v.cursor.curSelection[1] ||
717 charNum < v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
719 selectStyle := defStyle.Reverse(true)
721 if style, ok := colorscheme["selection"]; ok {
724 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
729 // v.lastMatches = matches
732 // Display renders the view, the cursor, and statusline
733 func (v *View) Display() {