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 value for the cursor, and the window that the user sees
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
92 v.eh = NewEventHandler(v)
98 // Update the syntax highlighting for the entire buffer at the start
99 v.UpdateLines(v.topline, v.topline+v.height)
102 // Set mouseReleased to true because we assume the mouse is not being pressed when
103 // the editor is opened
104 v.mouseReleased = true
105 v.lastClickTime = time.Time{}
110 // UpdateLines sets the values for v.updateLines
111 func (v *View) UpdateLines(start, end int) {
112 v.updateLines[0] = start
113 v.updateLines[1] = end + 1
116 // Resize recalculates the actual width and height of the view from the width and height
118 // This is usually called when the window is resized, or when a split has been added and
119 // the percentages have changed
120 func (v *View) Resize(w, h int) {
121 // Always include 1 line for the command line at the bottom
123 v.width = int(float32(w) * float32(v.widthPercent) / 100)
124 // We subtract 1 for the statusline
125 v.height = int(float32(h)*float32(v.heightPercent)/100) - 1
128 // ScrollUp scrolls the view up n lines (if possible)
129 func (v *View) ScrollUp(n int) {
130 // Try to scroll by n but if it would overflow, scroll by 1
131 if v.topline-n >= 0 {
133 } else if v.topline > 0 {
138 // ScrollDown scrolls the view down n lines (if possible)
139 func (v *View) ScrollDown(n int) {
140 // Try to scroll by n but if it would overflow, scroll by 1
141 if v.topline+n <= len(v.buf.lines)-v.height {
143 } else if v.topline < len(v.buf.lines)-v.height {
148 // PageUp scrolls the view up a page
149 func (v *View) PageUp() {
150 if v.topline > v.height {
157 // PageDown scrolls the view down a page
158 func (v *View) PageDown() {
159 if len(v.buf.lines)-(v.topline+v.height) > v.height {
160 v.ScrollDown(v.height)
162 if len(v.buf.lines) >= v.height {
163 v.topline = len(v.buf.lines) - v.height
168 // HalfPageUp scrolls the view up half a page
169 func (v *View) HalfPageUp() {
170 if v.topline > v.height/2 {
171 v.ScrollUp(v.height / 2)
177 // HalfPageDown scrolls the view down half a page
178 func (v *View) HalfPageDown() {
179 if len(v.buf.lines)-(v.topline+v.height) > v.height/2 {
180 v.ScrollDown(v.height / 2)
182 v.topline = len(v.buf.lines) - v.height
186 // CanClose returns whether or not the view can be closed
187 // If there are unsaved changes, the user will be asked if the view can be closed
188 // causing them to lose the unsaved changes
189 // The message is what to print after saying "You have unsaved changes. "
190 func (v *View) CanClose(msg string) bool {
192 quit, canceled := messenger.Prompt("You have unsaved changes. " + msg)
194 if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
204 // Save the buffer to disk
205 func (v *View) Save() {
206 // If this is an empty buffer, ask for a filename
207 if v.buf.path == "" {
208 filename, canceled := messenger.Prompt("Filename: ")
210 v.buf.path = filename
211 v.buf.name = filename
218 messenger.Error(err.Error())
222 // Copy the selection to the system clipboard
223 func (v *View) Copy() {
224 if v.cursor.HasSelection() {
225 if !clipboard.Unsupported {
226 clipboard.WriteAll(v.cursor.GetSelection())
228 messenger.Error("Clipboard is not supported on your system")
233 // Cut the selection to the system clipboard
234 func (v *View) Cut() {
235 if v.cursor.HasSelection() {
236 if !clipboard.Unsupported {
237 clipboard.WriteAll(v.cursor.GetSelection())
238 v.cursor.DeleteSelection()
239 v.cursor.ResetSelection()
241 messenger.Error("Clipboard is not supported on your system")
246 // Paste whatever is in the system clipboard into the buffer
247 // Delete and paste if the user has a selection
248 func (v *View) Paste() {
249 if !clipboard.Unsupported {
250 if v.cursor.HasSelection() {
251 v.cursor.DeleteSelection()
252 v.cursor.ResetSelection()
254 clip, _ := clipboard.ReadAll()
255 v.eh.Insert(v.cursor.Loc(), clip)
256 // This is a bit weird... Not sure if there's a better way
257 for i := 0; i < Count(clip); i++ {
261 messenger.Error("Clipboard is not supported on your system")
265 // SelectAll selects the entire buffer
266 func (v *View) SelectAll() {
267 v.cursor.curSelection[1] = 0
268 v.cursor.curSelection[0] = v.buf.Len()
269 // Put the cursor at the beginning
274 // OpenFile opens a new file in the current view
275 // It makes sure that the current buffer can be closed first (unsaved changes)
276 func (v *View) OpenFile() {
277 if v.CanClose("Continue? ") {
278 filename, canceled := messenger.Prompt("File to open: ")
282 file, err := ioutil.ReadFile(filename)
285 messenger.Error(err.Error())
288 v.buf = NewBuffer(string(file), filename)
292 // Relocate moves the view window so that the cursor is in view
293 // This is useful if the user has scrolled far away, and then starts typing
294 func (v *View) Relocate() {
299 if cy > v.topline+v.height-1 {
300 v.topline = cy - v.height + 1
304 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
306 func (v *View) MoveToMouseClick(x, y int) {
307 if y-v.topline > v.height-1 {
309 y = v.height + v.topline - 1
311 if y >= len(v.buf.lines) {
312 y = len(v.buf.lines) - 1
318 x = v.cursor.GetCharPosInLine(y, x)
319 if x > Count(v.buf.lines[y]) {
320 x = Count(v.buf.lines[y])
326 // HandleEvent handles an event passed by the main loop
327 func (v *View) HandleEvent(event tcell.Event) {
328 // This bool determines whether the view is relocated at the end of the function
329 // By default it's true because most events should cause a relocate
331 // By default we don't update and syntax highlighting
333 switch e := event.(type) {
334 case *tcell.EventResize:
337 case *tcell.EventKey:
353 v.eh.Insert(v.cursor.Loc(), "\n")
355 // Rehighlight the entire buffer
356 v.UpdateLines(v.topline, v.topline+v.height)
357 // v.UpdateLines(v.cursor.y-1, v.cursor.y)
360 v.eh.Insert(v.cursor.Loc(), " ")
362 v.UpdateLines(v.cursor.y, v.cursor.y)
363 case tcell.KeyBackspace2:
364 // Delete a character
365 if v.cursor.HasSelection() {
366 v.cursor.DeleteSelection()
367 v.cursor.ResetSelection()
368 // Rehighlight the entire buffer
369 v.UpdateLines(v.topline, v.topline+v.height)
370 } else if v.cursor.Loc() > 0 {
371 // We have to do something a bit hacky here because we want to
372 // delete the line by first moving left and then deleting backwards
373 // but the undo redo would place the cursor in the wrong place
374 // So instead we move left, save the position, move back, delete
375 // and restore the position
377 cx, cy := v.cursor.x, v.cursor.y
379 loc := v.cursor.Loc()
380 v.eh.Remove(loc-1, loc)
381 v.cursor.x, v.cursor.y = cx, cy
382 // Rehighlight the entire buffer
383 v.UpdateLines(v.topline, v.topline+v.height)
384 // v.UpdateLines(v.cursor.y, v.cursor.y+1)
388 v.eh.Insert(v.cursor.Loc(), "\t")
390 v.UpdateLines(v.cursor.y, v.cursor.y)
395 // Rehighlight the entire buffer
396 v.UpdateLines(v.topline, v.topline+v.height)
399 // Rehighlight the entire buffer
400 v.UpdateLines(v.topline, v.topline+v.height)
403 // Rehighlight the entire buffer
404 v.UpdateLines(v.topline, v.topline+v.height)
407 // Rehighlight the entire buffer
408 v.UpdateLines(v.topline, v.topline+v.height)
411 // Rehighlight the entire buffer
412 v.UpdateLines(v.topline, v.topline+v.height)
417 // Rehighlight the entire buffer
418 v.UpdateLines(v.topline, v.topline+v.height)
432 // Insert a character
433 if v.cursor.HasSelection() {
434 v.cursor.DeleteSelection()
435 v.cursor.ResetSelection()
436 // Rehighlight the entire buffer
437 v.UpdateLines(v.topline, v.topline+v.height)
439 v.UpdateLines(v.cursor.y, v.cursor.y)
441 v.eh.Insert(v.cursor.Loc(), string(e.Rune()))
444 case *tcell.EventMouse:
448 // Position always seems to be off by one
452 button := e.Buttons()
457 origX, origY := v.cursor.x, v.cursor.y
458 v.MoveToMouseClick(x, y)
461 if (time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold) &&
462 (origX == v.cursor.x && origY == v.cursor.y) {
465 v.lastClickTime = time.Now()
468 v.doubleClick = false
470 v.cursor.SelectLine()
473 v.lastClickTime = time.Now()
476 v.tripleClick = false
478 v.cursor.SelectWord()
481 v.doubleClick = false
482 v.tripleClick = false
483 v.lastClickTime = time.Now()
485 loc := v.cursor.Loc()
486 v.cursor.curSelection[0] = loc
487 v.cursor.curSelection[1] = loc
491 v.cursor.AddLineToSelection()
492 } else if v.doubleClick {
493 v.cursor.AddWordToSelection()
495 v.cursor.curSelection[1] = v.cursor.Loc()
498 v.mouseReleased = false
500 case tcell.ButtonNone:
501 // Mouse event with no click
502 if !v.mouseReleased {
503 // Mouse was just released
505 // Relocating here isn't really necessary because the cursor will
506 // be in the right place from the last mouse event
507 // However, if we are running in a terminal that doesn't support mouse motion
508 // events, this still allows the user to make selections, except only after they
511 if !v.doubleClick && !v.tripleClick {
512 v.MoveToMouseClick(x, y)
513 v.cursor.curSelection[1] = v.cursor.Loc()
515 v.mouseReleased = true
517 // We don't want to relocate because otherwise the view will be relocated
518 // every time the user moves the cursor
521 // Scroll up two lines
523 // We don't want to relocate if the user is scrolling
525 // Rehighlight the entire buffer
526 v.UpdateLines(v.topline, v.topline+v.height)
527 case tcell.WheelDown:
528 // Scroll down two lines
530 // We don't want to relocate if the user is scrolling
532 // Rehighlight the entire buffer
533 v.UpdateLines(v.topline, v.topline+v.height)
544 // DisplayView renders the view to the screen
545 func (v *View) DisplayView() {
546 // matches := make(SyntaxMatches, len(v.buf.lines))
548 // viewStart := v.topline
549 // viewEnd := v.topline + v.height
550 // if viewEnd > len(v.buf.lines) {
551 // viewEnd = len(v.buf.lines)
554 // lines := v.buf.lines[viewStart:viewEnd]
555 // for i, line := range lines {
556 // matches[i] = make([]tcell.Style, len(line))
559 // The character number of the character in the top left of the screen
560 charNum := ToCharPos(0, v.topline, v.buf)
562 // Convert the length of buffer to a string, and get the length of the string
563 // We are going to have to offset by that amount
564 maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
565 // + 1 for the little space after the line number
566 v.lineNumOffset = maxLineLength + 1
568 var highlightStyle tcell.Style
570 for lineN := 0; lineN < v.height; lineN++ {
572 // If the buffer is smaller than the view height
573 // and we went too far, break
574 if lineN+v.topline >= len(v.buf.lines) {
577 line := v.buf.lines[lineN+v.topline]
579 // Write the line number
580 lineNumStyle := tcell.StyleDefault
581 if style, ok := colorscheme["line-number"]; ok {
584 // Write the spaces before the line number if necessary
585 lineNum := strconv.Itoa(lineN + v.topline + 1)
586 for i := 0; i < maxLineLength-len(lineNum); i++ {
587 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
590 // Write the actual line number
591 for _, ch := range lineNum {
592 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
595 // Write the extra space
596 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
601 for colN, ch := range line {
602 var lineStyle tcell.Style
603 // Does the current character need to be syntax highlighted?
605 // if lineN >= v.updateLines[0] && lineN < v.updateLines[1] {
606 highlightStyle = v.matches[lineN][colN]
607 // } else if lineN < len(v.lastMatches) && colN < len(v.lastMatches[lineN]) {
608 // highlightStyle = v.lastMatches[lineN][colN]
610 // highlightStyle = tcell.StyleDefault
613 if v.cursor.HasSelection() &&
614 (charNum >= v.cursor.curSelection[0] && charNum <= v.cursor.curSelection[1] ||
615 charNum <= v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
617 lineStyle = tcell.StyleDefault.Reverse(true)
619 if style, ok := colorscheme["selection"]; ok {
623 lineStyle = highlightStyle
625 // matches[lineN][colN] = highlightStyle
628 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
629 tabSize := options["tabsize"].(int)
630 for i := 0; i < tabSize-1; i++ {
632 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
635 screen.SetContent(x+tabchars, lineN, ch, nil, lineStyle)
640 // Here we are at a newline
642 // The newline may be selected, in which case we should draw the selection style
643 // with a space to represent it
644 if v.cursor.HasSelection() &&
645 (charNum >= v.cursor.curSelection[0] && charNum <= v.cursor.curSelection[1] ||
646 charNum <= v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
648 selectStyle := tcell.StyleDefault.Reverse(true)
650 if style, ok := colorscheme["selection"]; ok {
653 screen.SetContent(x+tabchars, lineN, ' ', nil, selectStyle)
658 // v.lastMatches = matches
661 // Display renders the view, the cursor, and statusline
662 func (v *View) Display() {