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
337 x = v.cursor.GetCharPosInLine(y, x)
338 if x > Count(v.buf.lines[y]) {
339 x = Count(v.buf.lines[y])
343 v.cursor.lastVisualX = v.cursor.GetVisualX()
346 // HandleEvent handles an event passed by the main loop
347 func (v *View) HandleEvent(event tcell.Event) {
348 // This bool determines whether the view is relocated at the end of the function
349 // By default it's true because most events should cause a relocate
352 // By default we don't update and syntax highlighting
354 switch e := event.(type) {
355 case *tcell.EventResize:
358 case *tcell.EventKey:
362 v.cursor.ResetSelection()
366 v.cursor.ResetSelection()
370 v.cursor.ResetSelection()
374 v.cursor.ResetSelection()
378 if v.cursor.HasSelection() {
379 v.cursor.DeleteSelection()
380 v.cursor.ResetSelection()
382 v.eh.Insert(v.cursor.Loc(), "\n")
384 // Rehighlight the entire buffer
385 v.UpdateLines(v.topline, v.topline+v.height)
386 v.cursor.lastVisualX = v.cursor.GetVisualX()
387 // v.UpdateLines(v.cursor.y-1, v.cursor.y)
390 if v.cursor.HasSelection() {
391 v.cursor.DeleteSelection()
392 v.cursor.ResetSelection()
394 v.eh.Insert(v.cursor.Loc(), " ")
396 v.UpdateLines(v.cursor.y, v.cursor.y)
397 case tcell.KeyBackspace2, tcell.KeyBackspace:
398 // Delete a character
399 if v.cursor.HasSelection() {
400 v.cursor.DeleteSelection()
401 v.cursor.ResetSelection()
402 // Rehighlight the entire buffer
403 v.UpdateLines(v.topline, v.topline+v.height)
404 } else if v.cursor.Loc() > 0 {
405 // We have to do something a bit hacky here because we want to
406 // delete the line by first moving left and then deleting backwards
407 // but the undo redo would place the cursor in the wrong place
408 // So instead we move left, save the position, move back, delete
409 // and restore the position
411 cx, cy := v.cursor.x, v.cursor.y
413 loc := v.cursor.Loc()
414 v.eh.Remove(loc-1, loc)
415 v.cursor.x, v.cursor.y = cx, cy
416 // Rehighlight the entire buffer
417 v.UpdateLines(v.topline, v.topline+v.height)
418 // v.UpdateLines(v.cursor.y, v.cursor.y+1)
420 v.cursor.lastVisualX = v.cursor.GetVisualX()
423 if v.cursor.HasSelection() {
424 v.cursor.DeleteSelection()
425 v.cursor.ResetSelection()
427 if settings.TabsToSpaces {
428 v.eh.Insert(v.cursor.Loc(), Spaces(settings.TabSize))
429 for i := 0; i < settings.TabSize; i++ {
433 v.eh.Insert(v.cursor.Loc(), "\t")
436 v.UpdateLines(v.cursor.y, v.cursor.y)
440 if v.cursor.HasSelection() {
441 searchStart = v.cursor.curSelection[1]
443 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
447 if v.cursor.HasSelection() {
448 searchStart = v.cursor.curSelection[1]
450 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
452 messenger.Message("Find: " + lastSearch)
453 Search(lastSearch, v, true)
455 if v.cursor.HasSelection() {
456 searchStart = v.cursor.curSelection[0]
458 searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
460 messenger.Message("Find: " + lastSearch)
461 Search(lastSearch, v, false)
464 // Rehighlight the entire buffer
465 v.UpdateLines(v.topline, v.topline+v.height)
468 // Rehighlight the entire buffer
469 v.UpdateLines(v.topline, v.topline+v.height)
472 // Rehighlight the entire buffer
473 v.UpdateLines(v.topline, v.topline+v.height)
476 // Rehighlight the entire buffer
477 v.UpdateLines(v.topline, v.topline+v.height)
480 // Rehighlight the entire buffer
481 v.UpdateLines(v.topline, v.topline+v.height)
486 // Rehighlight the entire buffer
487 v.UpdateLines(v.topline, v.topline+v.height)
492 v.topline = len(v.buf.lines) - 1 - v.height
507 // Insert a character
508 if v.cursor.HasSelection() {
509 v.cursor.DeleteSelection()
510 v.cursor.ResetSelection()
511 // Rehighlight the entire buffer
512 v.UpdateLines(v.topline, v.topline+v.height)
514 v.UpdateLines(v.cursor.y, v.cursor.y)
516 v.eh.Insert(v.cursor.Loc(), string(e.Rune()))
519 case *tcell.EventMouse:
521 x -= v.lineNumOffset - v.leftCol
523 // Position always seems to be off by one
527 button := e.Buttons()
532 origX, origY := v.cursor.x, v.cursor.y
533 v.MoveToMouseClick(x, y)
536 if (time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold) &&
537 (origX == v.cursor.x && origY == v.cursor.y) {
540 v.lastClickTime = time.Now()
543 v.doubleClick = false
545 v.cursor.SelectLine()
548 v.lastClickTime = time.Now()
551 v.tripleClick = false
553 v.cursor.SelectWord()
556 v.doubleClick = false
557 v.tripleClick = false
558 v.lastClickTime = time.Now()
560 loc := v.cursor.Loc()
561 v.cursor.curSelection[0] = loc
562 v.cursor.curSelection[1] = loc
566 v.cursor.AddLineToSelection()
567 } else if v.doubleClick {
568 v.cursor.AddWordToSelection()
570 v.cursor.curSelection[1] = v.cursor.Loc()
573 v.mouseReleased = false
574 case tcell.ButtonNone:
575 // Mouse event with no click
576 if !v.mouseReleased {
577 // Mouse was just released
579 // Relocating here isn't really necessary because the cursor will
580 // be in the right place from the last mouse event
581 // However, if we are running in a terminal that doesn't support mouse motion
582 // events, this still allows the user to make selections, except only after they
585 if !v.doubleClick && !v.tripleClick {
586 v.MoveToMouseClick(x, y)
587 v.cursor.curSelection[1] = v.cursor.Loc()
589 v.mouseReleased = true
591 // We don't want to relocate because otherwise the view will be relocated
592 // every time the user moves the cursor
595 // Scroll up two lines
597 // We don't want to relocate if the user is scrolling
599 // Rehighlight the entire buffer
600 v.UpdateLines(v.topline, v.topline+v.height)
601 case tcell.WheelDown:
602 // Scroll down two lines
604 // We don't want to relocate if the user is scrolling
606 // Rehighlight the entire buffer
607 v.UpdateLines(v.topline, v.topline+v.height)
619 // DisplayView renders the view to the screen
620 func (v *View) DisplayView() {
621 // matches := make(SyntaxMatches, len(v.buf.lines))
623 // viewStart := v.topline
624 // viewEnd := v.topline + v.height
625 // if viewEnd > len(v.buf.lines) {
626 // viewEnd = len(v.buf.lines)
629 // lines := v.buf.lines[viewStart:viewEnd]
630 // for i, line := range lines {
631 // matches[i] = make([]tcell.Style, len(line))
634 // The character number of the character in the top left of the screen
636 charNum := ToCharPos(0, v.topline, v.buf)
638 // Convert the length of buffer to a string, and get the length of the string
639 // We are going to have to offset by that amount
640 maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
641 // + 1 for the little space after the line number
642 v.lineNumOffset = maxLineLength + 1
644 var highlightStyle tcell.Style
646 for lineN := 0; lineN < v.height; lineN++ {
648 // If the buffer is smaller than the view height
649 // and we went too far, break
650 if lineN+v.topline >= len(v.buf.lines) {
653 line := v.buf.lines[lineN+v.topline]
655 // Write the line number
656 lineNumStyle := defStyle
657 if style, ok := colorscheme["line-number"]; ok {
660 // Write the spaces before the line number if necessary
661 lineNum := strconv.Itoa(lineN + v.topline + 1)
662 for i := 0; i < maxLineLength-len(lineNum); i++ {
663 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
666 // Write the actual line number
667 for _, ch := range lineNum {
668 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
671 // Write the extra space
672 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
677 runes := []rune(line)
678 for colN := v.leftCol; colN < v.leftCol+v.width; colN++ {
679 if colN >= len(runes) {
683 var lineStyle tcell.Style
684 // Does the current character need to be syntax highlighted?
686 // if lineN >= v.updateLines[0] && lineN < v.updateLines[1] {
688 highlightStyle = v.matches[lineN][colN]
690 // } else if lineN < len(v.lastMatches) && colN < len(v.lastMatches[lineN]) {
691 // highlightStyle = v.lastMatches[lineN][colN]
693 // highlightStyle = defStyle
696 if v.cursor.HasSelection() &&
697 (charNum >= v.cursor.curSelection[0] && charNum < v.cursor.curSelection[1] ||
698 charNum < v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
700 lineStyle = defStyle.Reverse(true)
702 if style, ok := colorscheme["selection"]; ok {
706 lineStyle = highlightStyle
708 // matches[lineN][colN] = highlightStyle
711 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
712 tabSize := settings.TabSize
713 for i := 0; i < tabSize-1; i++ {
715 if x-v.leftCol+tabchars >= v.lineNumOffset {
716 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
720 if x-v.leftCol+tabchars >= v.lineNumOffset {
721 screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
727 // Here we are at a newline
729 // The newline may be selected, in which case we should draw the selection style
730 // with a space to represent it
731 if v.cursor.HasSelection() &&
732 (charNum >= v.cursor.curSelection[0] && charNum < v.cursor.curSelection[1] ||
733 charNum < v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
735 selectStyle := defStyle.Reverse(true)
737 if style, ok := colorscheme["selection"]; ok {
740 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
745 // v.lastMatches = matches
748 // Display renders the view, the cursor, and statusline
749 func (v *View) Display() {