4 "github.com/atotto/clipboard"
5 "github.com/gdamore/tcell"
11 // The View struct stores information about a view into a buffer.
12 // It has a value for the cursor, and the window that the user sees
17 // The topmost line, used for vertical scrolling
19 // The leftmost column, used for horizontal scrolling
22 // Percentage of the terminal window that this view takes up (from 0 to 100)
26 // Actual with and height
30 // How much to offset because of line numbers
33 // The eventhandler for undo/redo
41 // Since tcell doesn't differentiate between a mouse release event
42 // and a mouse move event with no keys pressed, we need to keep
43 // track of whether or not the mouse was pressed (or not released) last event to determine
44 // mouse release events
47 // Syntax higlighting matches
50 // The messenger so we can send messages to the user and get input from them
54 // NewView returns a new fullscreen view
55 func NewView(buf *Buffer, m *Messenger) *View {
56 return NewViewWidthHeight(buf, m, 100, 100)
59 // NewViewWidthHeight returns a new view with the specified width and height percentages
60 // Note that w and h are percentages not actual values
61 func NewViewWidthHeight(buf *Buffer, m *Messenger, w, h int) *View {
70 v.Resize(screen.Size())
73 // Put the cursor at the first spot
81 v.eh = NewEventHandler(v)
90 // Resize recalculates the actual width and height of the view from the width and height
92 // This is usually called when the window is resized, or when a split has been added and
93 // the percentages have changed
94 func (v *View) Resize(w, h int) {
95 // Always include 1 line for the command line at the bottom
97 v.width = int(float32(w) * float32(v.widthPercent) / 100)
98 // We subtract 1 for the statusline
99 v.height = int(float32(h)*float32(v.heightPercent)/100) - 1
102 // ScrollUp scrolls the view up n lines (if possible)
103 func (v *View) ScrollUp(n int) {
104 // Try to scroll by n but if it would overflow, scroll by 1
105 if v.topline-n >= 0 {
107 } else if v.topline > 0 {
112 // ScrollDown scrolls the view down n lines (if possible)
113 func (v *View) ScrollDown(n int) {
114 // Try to scroll by n but if it would overflow, scroll by 1
115 if v.topline+n <= len(v.buf.lines)-v.height {
117 } else if v.topline < len(v.buf.lines)-v.height {
122 // PageUp scrolls the view up a page
123 func (v *View) PageUp() {
124 if v.topline > v.height {
131 // PageDown scrolls the view down a page
132 func (v *View) PageDown() {
133 if len(v.buf.lines)-(v.topline+v.height) > v.height {
134 v.ScrollDown(v.height)
136 v.topline = len(v.buf.lines) - v.height
140 // HalfPageUp scrolls the view up half a page
141 func (v *View) HalfPageUp() {
142 if v.topline > v.height/2 {
143 v.ScrollUp(v.height / 2)
149 // HalfPageDown scrolls the view down half a page
150 func (v *View) HalfPageDown() {
151 if len(v.buf.lines)-(v.topline+v.height) > v.height/2 {
152 v.ScrollDown(v.height / 2)
154 v.topline = len(v.buf.lines) - v.height
158 // CanClose returns whether or not the view can be closed
159 // If there are unsaved changes, the user will be asked if the view can be closed
160 // causing them to lose the unsaved changes
161 // The message is what to print after saying "You have unsaved changes. "
162 func (v *View) CanClose(msg string) bool {
164 quit, canceled := v.m.Prompt("You have unsaved changes. " + msg)
166 if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
176 // Save the buffer to disk
177 func (v *View) Save() {
178 // If this is an empty buffer, ask for a filename
179 if v.buf.path == "" {
180 filename, canceled := v.m.Prompt("Filename: ")
182 v.buf.path = filename
183 v.buf.name = filename
190 v.m.Error(err.Error())
194 // Copy the selection to the system clipboard
195 func (v *View) Copy() {
196 if v.cursor.HasSelection() {
197 if !clipboard.Unsupported {
198 clipboard.WriteAll(v.cursor.GetSelection())
200 v.m.Error("Clipboard is not supported on your system")
205 // Cut the selection to the system clipboard
206 func (v *View) Cut() {
207 if v.cursor.HasSelection() {
208 if !clipboard.Unsupported {
209 clipboard.WriteAll(v.cursor.GetSelection())
210 v.cursor.DeleteSelection()
211 v.cursor.ResetSelection()
213 v.m.Error("Clipboard is not supported on your system")
218 // Paste whatever is in the system clipboard into the buffer
219 // Delete and paste if the user has a selection
220 func (v *View) Paste() {
221 if !clipboard.Unsupported {
222 if v.cursor.HasSelection() {
223 v.cursor.DeleteSelection()
224 v.cursor.ResetSelection()
226 clip, _ := clipboard.ReadAll()
227 v.eh.Insert(v.cursor.loc, clip)
228 // This is a bit weird... Not sure if there's a better way
229 for i := 0; i < Count(clip); i++ {
233 v.m.Error("Clipboard is not supported on your system")
237 // SelectAll selects the entire buffer
238 func (v *View) SelectAll() {
239 v.cursor.selectionEnd = 0
240 v.cursor.selectionStart = v.buf.Len()
241 // Put the cursor at the beginning
247 // OpenFile opens a new file in the current view
248 // It makes sure that the current buffer can be closed first (unsaved changes)
249 func (v *View) OpenFile() {
250 if v.CanClose("Continue? ") {
251 filename, canceled := v.m.Prompt("File to open: ")
255 file, err := ioutil.ReadFile(filename)
258 v.m.Error(err.Error())
261 v.buf = NewBuffer(string(file), filename)
265 // Relocate moves the view window so that the cursor is in view
266 // This is useful if the user has scrolled far away, and then starts typing
267 func (v *View) Relocate() {
272 if cy > v.topline+v.height-1 {
273 v.topline = cy - v.height + 1
277 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
279 func (v *View) MoveToMouseClick(x, y int) {
280 if y-v.topline > v.height-1 {
282 y = v.height + v.topline - 1
284 if y >= len(v.buf.lines) {
285 y = len(v.buf.lines) - 1
291 x = v.cursor.GetCharPosInLine(y, x)
292 if x > Count(v.buf.lines[y]) {
293 x = Count(v.buf.lines[y])
295 d := v.cursor.Distance(x, y)
301 // HandleEvent handles an event passed by the main loop
302 func (v *View) HandleEvent(event tcell.Event) {
303 // This bool determines whether the view is relocated at the end of the function
304 // By default it's true because most events should cause a relocate
306 switch e := event.(type) {
307 case *tcell.EventResize:
310 case *tcell.EventKey:
326 v.eh.Insert(v.cursor.loc, "\n")
330 v.eh.Insert(v.cursor.loc, " ")
332 case tcell.KeyBackspace2:
333 // Delete a character
334 if v.cursor.HasSelection() {
335 v.cursor.DeleteSelection()
336 v.cursor.ResetSelection()
337 } else if v.cursor.loc > 0 {
338 // We have to do something a bit hacky here because we want to
339 // delete the line by first moving left and then deleting backwards
340 // but the undo redo would place the cursor in the wrong place
341 // So instead we move left, save the position, move back, delete
342 // and restore the position
344 cx, cy, cloc := v.cursor.x, v.cursor.y, v.cursor.loc
346 v.eh.Remove(v.cursor.loc-1, v.cursor.loc)
347 v.cursor.x, v.cursor.y, v.cursor.loc = cx, cy, cloc
351 v.eh.Insert(v.cursor.loc, "\t")
378 // Insert a character
379 if v.cursor.HasSelection() {
380 v.cursor.DeleteSelection()
381 v.cursor.ResetSelection()
383 v.eh.Insert(v.cursor.loc, string(e.Rune()))
386 case *tcell.EventMouse:
390 // Position always seems to be off by one
394 button := e.Buttons()
399 v.MoveToMouseClick(x, y)
402 v.cursor.selectionStart = v.cursor.loc
403 v.cursor.selectionStartX = v.cursor.x
404 v.cursor.selectionStartY = v.cursor.y
406 v.cursor.selectionEnd = v.cursor.loc
407 v.mouseReleased = false
408 case tcell.ButtonNone:
409 // Mouse event with no click
410 if !v.mouseReleased {
411 // Mouse was just released
413 // Relocating here isn't really necessary because the cursor will
414 // be in the right place from the last mouse event
415 // However, if we are running in a terminal that doesn't support mouse motion
416 // events, this still allows the user to make selections, except only after they
418 v.MoveToMouseClick(x, y)
419 v.cursor.selectionEnd = v.cursor.loc
420 v.mouseReleased = true
422 // We don't want to relocate because otherwise the view will be relocated
423 // everytime the user moves the cursor
426 // Scroll up two lines
428 // We don't want to relocate if the user is scrolling
430 case tcell.WheelDown:
431 // Scroll down two lines
433 // We don't want to relocate if the user is scrolling
443 // DisplayView renders the view to the screen
444 func (v *View) DisplayView() {
445 // The character number of the character in the top left of the screen
446 charNum := v.cursor.loc + v.cursor.Distance(0, v.topline)
448 // Convert the length of buffer to a string, and get the length of the string
449 // We are going to have to offset by that amount
450 maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
451 // + 1 for the little space after the line number
452 v.lineNumOffset = maxLineLength + 1
454 var highlightStyle tcell.Style
456 for lineN := 0; lineN < v.height; lineN++ {
458 // If the buffer is smaller than the view height
459 // and we went too far, break
460 if lineN+v.topline >= len(v.buf.lines) {
463 line := v.buf.lines[lineN+v.topline]
465 // Write the line number
466 lineNumStyle := tcell.StyleDefault
467 if _, ok := colorscheme["line-number"]; ok {
468 lineNumStyle = colorscheme["line-number"]
470 // Write the spaces before the line number if necessary
471 lineNum := strconv.Itoa(lineN + v.topline + 1)
472 for i := 0; i < maxLineLength-len(lineNum); i++ {
473 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
476 // Write the actual line number
477 for _, ch := range lineNum {
478 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
481 // Write the extra space
482 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
487 for _, ch := range line {
488 var lineStyle tcell.Style
489 // Does the current character need to be syntax highlighted?
490 st, ok := v.matches[charNum]
494 highlightStyle = tcell.StyleDefault
497 if v.cursor.HasSelection() &&
498 (charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd ||
499 charNum <= v.cursor.selectionStart && charNum >= v.cursor.selectionEnd) {
501 lineStyle = tcell.StyleDefault.Reverse(true)
503 if _, ok := colorscheme["selection"]; ok {
504 lineStyle = colorscheme["selection"]
507 lineStyle = highlightStyle
511 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
512 for i := 0; i < tabSize-1; i++ {
514 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
517 screen.SetContent(x+tabchars, lineN, ch, nil, lineStyle)
522 // Here we are at a newline
524 // The newline may be selected, in which case we should draw the selection style
525 // with a space to represent it
526 if v.cursor.HasSelection() &&
527 (charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd ||
528 charNum <= v.cursor.selectionStart && charNum >= v.cursor.selectionEnd) {
530 selectStyle := tcell.StyleDefault.Reverse(true)
532 if _, ok := colorscheme["selection"]; ok {
533 selectStyle = colorscheme["selection"]
535 screen.SetContent(x+tabchars, lineN, ' ', nil, selectStyle)
543 // Display renders the view, the cursor, and statusline
544 func (v *View) Display() {