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 if len(v.buf.lines) >= v.height {
183 v.topline = len(v.buf.lines) - v.height
188 // CanClose returns whether or not the view can be closed
189 // If there are unsaved changes, the user will be asked if the view can be closed
190 // causing them to lose the unsaved changes
191 // The message is what to print after saying "You have unsaved changes. "
192 func (v *View) CanClose(msg string) bool {
194 quit, canceled := messenger.Prompt("You have unsaved changes. " + msg)
196 if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
206 // Save the buffer to disk
207 func (v *View) Save() {
208 // If this is an empty buffer, ask for a filename
209 if v.buf.path == "" {
210 filename, canceled := messenger.Prompt("Filename: ")
212 v.buf.path = filename
213 v.buf.name = filename
220 messenger.Error(err.Error())
222 messenger.Message("Saved " + v.buf.path)
225 // Copy the selection to the system clipboard
226 func (v *View) Copy() {
227 if v.cursor.HasSelection() {
228 if !clipboard.Unsupported {
229 clipboard.WriteAll(v.cursor.GetSelection())
231 messenger.Error("Clipboard is not supported on your system")
236 // Cut the selection to the system clipboard
237 func (v *View) Cut() {
238 if v.cursor.HasSelection() {
239 if !clipboard.Unsupported {
240 clipboard.WriteAll(v.cursor.GetSelection())
241 v.cursor.DeleteSelection()
242 v.cursor.ResetSelection()
244 messenger.Error("Clipboard is not supported on your system")
249 // Paste whatever is in the system clipboard into the buffer
250 // Delete and paste if the user has a selection
251 func (v *View) Paste() {
252 if !clipboard.Unsupported {
253 if v.cursor.HasSelection() {
254 v.cursor.DeleteSelection()
255 v.cursor.ResetSelection()
257 clip, _ := clipboard.ReadAll()
258 v.eh.Insert(v.cursor.Loc(), clip)
259 // This is a bit weird... Not sure if there's a better way
260 for i := 0; i < Count(clip); i++ {
264 messenger.Error("Clipboard is not supported on your system")
268 // SelectAll selects the entire buffer
269 func (v *View) SelectAll() {
270 v.cursor.curSelection[1] = 0
271 v.cursor.curSelection[0] = v.buf.Len()
272 // Put the cursor at the beginning
277 // OpenFile opens a new file in the current view
278 // It makes sure that the current buffer can be closed first (unsaved changes)
279 func (v *View) OpenFile() {
280 if v.CanClose("Continue? ") {
281 filename, canceled := messenger.Prompt("File to open: ")
285 file, err := ioutil.ReadFile(filename)
288 messenger.Error(err.Error())
291 v.buf = NewBuffer(string(file), filename)
295 // Relocate moves the view window so that the cursor is in view
296 // This is useful if the user has scrolled far away, and then starts typing
297 func (v *View) Relocate() {
302 if cy > v.topline+v.height-1 {
303 v.topline = cy - v.height + 1
306 cx := v.cursor.GetVisualX()
310 if cx+v.lineNumOffset+1 > v.leftCol+v.width {
311 v.leftCol = cx - v.width + v.lineNumOffset + 1
315 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
317 func (v *View) MoveToMouseClick(x, y int) {
318 if y-v.topline > v.height-1 {
320 y = v.height + v.topline - 1
322 if y >= len(v.buf.lines) {
323 y = len(v.buf.lines) - 1
329 x = v.cursor.GetCharPosInLine(y, x)
330 if x > Count(v.buf.lines[y]) {
331 x = Count(v.buf.lines[y])
335 v.cursor.lastVisualX = v.cursor.GetVisualX()
338 // HandleEvent handles an event passed by the main loop
339 func (v *View) HandleEvent(event tcell.Event) {
340 // This bool determines whether the view is relocated at the end of the function
341 // By default it's true because most events should cause a relocate
343 // By default we don't update and syntax highlighting
345 switch e := event.(type) {
346 case *tcell.EventResize:
349 case *tcell.EventKey:
365 v.eh.Insert(v.cursor.Loc(), "\n")
367 // Rehighlight the entire buffer
368 v.UpdateLines(v.topline, v.topline+v.height)
369 // v.UpdateLines(v.cursor.y-1, v.cursor.y)
372 v.eh.Insert(v.cursor.Loc(), " ")
374 v.UpdateLines(v.cursor.y, v.cursor.y)
375 case tcell.KeyBackspace2:
376 // Delete a character
377 if v.cursor.HasSelection() {
378 v.cursor.DeleteSelection()
379 v.cursor.ResetSelection()
380 // Rehighlight the entire buffer
381 v.UpdateLines(v.topline, v.topline+v.height)
382 } else if v.cursor.Loc() > 0 {
383 // We have to do something a bit hacky here because we want to
384 // delete the line by first moving left and then deleting backwards
385 // but the undo redo would place the cursor in the wrong place
386 // So instead we move left, save the position, move back, delete
387 // and restore the position
389 cx, cy := v.cursor.x, v.cursor.y
391 loc := v.cursor.Loc()
392 v.eh.Remove(loc-1, loc)
393 v.cursor.x, v.cursor.y = cx, cy
394 // Rehighlight the entire buffer
395 v.UpdateLines(v.topline, v.topline+v.height)
396 // v.UpdateLines(v.cursor.y, v.cursor.y+1)
400 v.eh.Insert(v.cursor.Loc(), "\t")
402 v.UpdateLines(v.cursor.y, v.cursor.y)
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)
415 // Rehighlight the entire buffer
416 v.UpdateLines(v.topline, v.topline+v.height)
419 // Rehighlight the entire buffer
420 v.UpdateLines(v.topline, v.topline+v.height)
423 // Rehighlight the entire buffer
424 v.UpdateLines(v.topline, v.topline+v.height)
429 // Rehighlight the entire buffer
430 v.UpdateLines(v.topline, v.topline+v.height)
444 // Insert a character
445 if v.cursor.HasSelection() {
446 v.cursor.DeleteSelection()
447 v.cursor.ResetSelection()
448 // Rehighlight the entire buffer
449 v.UpdateLines(v.topline, v.topline+v.height)
451 v.UpdateLines(v.cursor.y, v.cursor.y)
453 v.eh.Insert(v.cursor.Loc(), string(e.Rune()))
456 case *tcell.EventMouse:
458 x -= v.lineNumOffset - v.leftCol
460 // Position always seems to be off by one
464 button := e.Buttons()
469 origX, origY := v.cursor.x, v.cursor.y
470 v.MoveToMouseClick(x, y)
473 if (time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold) &&
474 (origX == v.cursor.x && origY == v.cursor.y) {
477 v.lastClickTime = time.Now()
480 v.doubleClick = false
482 v.cursor.SelectLine()
485 v.lastClickTime = time.Now()
488 v.tripleClick = false
490 v.cursor.SelectWord()
493 v.doubleClick = false
494 v.tripleClick = false
495 v.lastClickTime = time.Now()
497 loc := v.cursor.Loc()
498 v.cursor.curSelection[0] = loc
499 v.cursor.curSelection[1] = loc
503 v.cursor.AddLineToSelection()
504 } else if v.doubleClick {
505 v.cursor.AddWordToSelection()
507 v.cursor.curSelection[1] = v.cursor.Loc()
510 v.mouseReleased = false
512 case tcell.ButtonNone:
513 // Mouse event with no click
514 if !v.mouseReleased {
515 // Mouse was just released
517 // Relocating here isn't really necessary because the cursor will
518 // be in the right place from the last mouse event
519 // However, if we are running in a terminal that doesn't support mouse motion
520 // events, this still allows the user to make selections, except only after they
523 if !v.doubleClick && !v.tripleClick {
524 v.MoveToMouseClick(x, y)
525 v.cursor.curSelection[1] = v.cursor.Loc()
527 v.mouseReleased = true
529 // We don't want to relocate because otherwise the view will be relocated
530 // every time the user moves the cursor
533 // Scroll up two lines
535 // We don't want to relocate if the user is scrolling
537 // Rehighlight the entire buffer
538 v.UpdateLines(v.topline, v.topline+v.height)
539 case tcell.WheelDown:
540 // Scroll down two lines
542 // We don't want to relocate if the user is scrolling
544 // Rehighlight the entire buffer
545 v.UpdateLines(v.topline, v.topline+v.height)
556 // DisplayView renders the view to the screen
557 func (v *View) DisplayView() {
558 // matches := make(SyntaxMatches, len(v.buf.lines))
560 // viewStart := v.topline
561 // viewEnd := v.topline + v.height
562 // if viewEnd > len(v.buf.lines) {
563 // viewEnd = len(v.buf.lines)
566 // lines := v.buf.lines[viewStart:viewEnd]
567 // for i, line := range lines {
568 // matches[i] = make([]tcell.Style, len(line))
571 // The character number of the character in the top left of the screen
572 charNum := ToCharPos(0, v.topline, v.buf)
574 // Convert the length of buffer to a string, and get the length of the string
575 // We are going to have to offset by that amount
576 maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
577 // + 1 for the little space after the line number
578 v.lineNumOffset = maxLineLength + 1
580 var highlightStyle tcell.Style
582 for lineN := 0; lineN < v.height; lineN++ {
584 // If the buffer is smaller than the view height
585 // and we went too far, break
586 if lineN+v.topline >= len(v.buf.lines) {
589 line := v.buf.lines[lineN+v.topline]
591 // Write the line number
592 lineNumStyle := tcell.StyleDefault
593 if style, ok := colorscheme["line-number"]; ok {
596 // Write the spaces before the line number if necessary
597 lineNum := strconv.Itoa(lineN + v.topline + 1)
598 for i := 0; i < maxLineLength-len(lineNum); i++ {
599 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
602 // Write the actual line number
603 for _, ch := range lineNum {
604 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
607 // Write the extra space
608 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
613 for colN, ch := range line {
614 var lineStyle tcell.Style
615 // Does the current character need to be syntax highlighted?
617 // if lineN >= v.updateLines[0] && lineN < v.updateLines[1] {
618 highlightStyle = v.matches[lineN][colN]
619 // } else if lineN < len(v.lastMatches) && colN < len(v.lastMatches[lineN]) {
620 // highlightStyle = v.lastMatches[lineN][colN]
622 // highlightStyle = tcell.StyleDefault
625 if v.cursor.HasSelection() &&
626 (charNum >= v.cursor.curSelection[0] && charNum <= v.cursor.curSelection[1] ||
627 charNum <= v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
629 lineStyle = tcell.StyleDefault.Reverse(true)
631 if style, ok := colorscheme["selection"]; ok {
635 lineStyle = highlightStyle
637 // matches[lineN][colN] = highlightStyle
640 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
641 tabSize := settings.TabSize
642 for i := 0; i < tabSize-1; i++ {
644 if x-v.leftCol+tabchars >= v.lineNumOffset {
645 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
649 if x-v.leftCol+tabchars >= v.lineNumOffset {
650 screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
656 // Here we are at a newline
658 // The newline may be selected, in which case we should draw the selection style
659 // with a space to represent it
660 if v.cursor.HasSelection() &&
661 (charNum >= v.cursor.curSelection[0] && charNum <= v.cursor.curSelection[1] ||
662 charNum <= v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
664 selectStyle := tcell.StyleDefault.Reverse(true)
666 if style, ok := colorscheme["selection"]; ok {
669 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
674 // v.lastMatches = matches
677 // Display renders the view, the cursor, and statusline
678 func (v *View) Display() {