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
49 // The matches from the last frame
50 lastMatches SyntaxMatches
52 // This is the range of lines that should have their syntax highlighting updated
55 // The messenger so we can send messages to the user and get input from them
59 // NewView returns a new fullscreen view
60 func NewView(buf *Buffer, m *Messenger) *View {
61 return NewViewWidthHeight(buf, m, 100, 100)
64 // NewViewWidthHeight returns a new view with the specified width and height percentages
65 // Note that w and h are percentages not actual values
66 func NewViewWidthHeight(buf *Buffer, m *Messenger, w, h int) *View {
75 v.Resize(screen.Size())
78 // Put the cursor at the first spot
86 v.eh = NewEventHandler(v)
92 // Update the syntax highlighting for the entire buffer at the start
93 v.UpdateLines(v.topline, v.topline+v.height)
94 // v.matches = Match(v.buf.rules, v.buf, v)
96 // Set mouseReleased to true because we assume the mouse is not being pressed when
97 // the editor is opened
98 v.mouseReleased = true
103 // UpdateLines sets the values for v.updateLines
104 func (v *View) UpdateLines(start, end int) {
105 v.updateLines[0] = start
106 v.updateLines[1] = end
109 // Resize recalculates the actual width and height of the view from the width and height
111 // This is usually called when the window is resized, or when a split has been added and
112 // the percentages have changed
113 func (v *View) Resize(w, h int) {
114 // Always include 1 line for the command line at the bottom
116 v.width = int(float32(w) * float32(v.widthPercent) / 100)
117 // We subtract 1 for the statusline
118 v.height = int(float32(h)*float32(v.heightPercent)/100) - 1
121 // ScrollUp scrolls the view up n lines (if possible)
122 func (v *View) ScrollUp(n int) {
123 // Try to scroll by n but if it would overflow, scroll by 1
124 if v.topline-n >= 0 {
126 } else if v.topline > 0 {
131 // ScrollDown scrolls the view down n lines (if possible)
132 func (v *View) ScrollDown(n int) {
133 // Try to scroll by n but if it would overflow, scroll by 1
134 if v.topline+n <= len(v.buf.lines)-v.height {
136 } else if v.topline < len(v.buf.lines)-v.height {
141 // PageUp scrolls the view up a page
142 func (v *View) PageUp() {
143 if v.topline > v.height {
150 // PageDown scrolls the view down a page
151 func (v *View) PageDown() {
152 if len(v.buf.lines)-(v.topline+v.height) > v.height {
153 v.ScrollDown(v.height)
155 v.topline = len(v.buf.lines) - v.height
159 // HalfPageUp scrolls the view up half a page
160 func (v *View) HalfPageUp() {
161 if v.topline > v.height/2 {
162 v.ScrollUp(v.height / 2)
168 // HalfPageDown scrolls the view down half a page
169 func (v *View) HalfPageDown() {
170 if len(v.buf.lines)-(v.topline+v.height) > v.height/2 {
171 v.ScrollDown(v.height / 2)
173 v.topline = len(v.buf.lines) - v.height
177 // CanClose returns whether or not the view can be closed
178 // If there are unsaved changes, the user will be asked if the view can be closed
179 // causing them to lose the unsaved changes
180 // The message is what to print after saying "You have unsaved changes. "
181 func (v *View) CanClose(msg string) bool {
183 quit, canceled := v.m.Prompt("You have unsaved changes. " + msg)
185 if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
195 // Save the buffer to disk
196 func (v *View) Save() {
197 // If this is an empty buffer, ask for a filename
198 if v.buf.path == "" {
199 filename, canceled := v.m.Prompt("Filename: ")
201 v.buf.path = filename
202 v.buf.name = filename
209 v.m.Error(err.Error())
213 // Copy the selection to the system clipboard
214 func (v *View) Copy() {
215 if v.cursor.HasSelection() {
216 if !clipboard.Unsupported {
217 clipboard.WriteAll(v.cursor.GetSelection())
219 v.m.Error("Clipboard is not supported on your system")
224 // Cut the selection to the system clipboard
225 func (v *View) Cut() {
226 if v.cursor.HasSelection() {
227 if !clipboard.Unsupported {
228 clipboard.WriteAll(v.cursor.GetSelection())
229 v.cursor.DeleteSelection()
230 v.cursor.ResetSelection()
232 v.m.Error("Clipboard is not supported on your system")
237 // Paste whatever is in the system clipboard into the buffer
238 // Delete and paste if the user has a selection
239 func (v *View) Paste() {
240 if !clipboard.Unsupported {
241 if v.cursor.HasSelection() {
242 v.cursor.DeleteSelection()
243 v.cursor.ResetSelection()
245 clip, _ := clipboard.ReadAll()
246 v.eh.Insert(v.cursor.loc, clip)
247 // This is a bit weird... Not sure if there's a better way
248 for i := 0; i < Count(clip); i++ {
252 v.m.Error("Clipboard is not supported on your system")
256 // SelectAll selects the entire buffer
257 func (v *View) SelectAll() {
258 v.cursor.selectionEnd = 0
259 v.cursor.selectionStart = v.buf.Len()
260 // Put the cursor at the beginning
266 // OpenFile opens a new file in the current view
267 // It makes sure that the current buffer can be closed first (unsaved changes)
268 func (v *View) OpenFile() {
269 if v.CanClose("Continue? ") {
270 filename, canceled := v.m.Prompt("File to open: ")
274 file, err := ioutil.ReadFile(filename)
277 v.m.Error(err.Error())
280 v.buf = NewBuffer(string(file), filename)
284 // Relocate moves the view window so that the cursor is in view
285 // This is useful if the user has scrolled far away, and then starts typing
286 func (v *View) Relocate() {
291 if cy > v.topline+v.height-1 {
292 v.topline = cy - v.height + 1
296 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
298 func (v *View) MoveToMouseClick(x, y int) {
299 if y-v.topline > v.height-1 {
301 y = v.height + v.topline - 1
303 if y >= len(v.buf.lines) {
304 y = len(v.buf.lines) - 1
310 x = v.cursor.GetCharPosInLine(y, x)
311 if x > Count(v.buf.lines[y]) {
312 x = Count(v.buf.lines[y])
314 d := v.cursor.Distance(x, y)
320 // HandleEvent handles an event passed by the main loop
321 func (v *View) HandleEvent(event tcell.Event) {
322 // This bool determines whether the view is relocated at the end of the function
323 // By default it's true because most events should cause a relocate
325 // By default we don't update and syntax highlighting
327 switch e := event.(type) {
328 case *tcell.EventResize:
331 case *tcell.EventKey:
347 v.eh.Insert(v.cursor.loc, "\n")
349 v.UpdateLines(v.cursor.y-1, v.cursor.y)
352 v.eh.Insert(v.cursor.loc, " ")
354 v.UpdateLines(v.cursor.y, v.cursor.y)
355 case tcell.KeyBackspace2:
356 // Delete a character
357 if v.cursor.HasSelection() {
358 v.cursor.DeleteSelection()
359 v.cursor.ResetSelection()
360 // Rehighlight the entire buffer
361 v.UpdateLines(v.topline, v.topline+v.height)
362 } else if v.cursor.loc > 0 {
363 // We have to do something a bit hacky here because we want to
364 // delete the line by first moving left and then deleting backwards
365 // but the undo redo would place the cursor in the wrong place
366 // So instead we move left, save the position, move back, delete
367 // and restore the position
369 cx, cy, cloc := v.cursor.x, v.cursor.y, v.cursor.loc
371 v.eh.Remove(v.cursor.loc-1, v.cursor.loc)
372 v.cursor.x, v.cursor.y, v.cursor.loc = cx, cy, cloc
373 v.UpdateLines(v.cursor.y, v.cursor.y+1)
377 v.eh.Insert(v.cursor.loc, "\t")
379 v.UpdateLines(v.cursor.y, v.cursor.y)
384 // Rehighlight the entire buffer
385 v.UpdateLines(v.topline, v.topline+v.height)
388 // Rehighlight the entire buffer
389 v.UpdateLines(v.topline, v.topline+v.height)
392 // Rehighlight the entire buffer
393 v.UpdateLines(v.topline, v.topline+v.height)
396 // Rehighlight the entire buffer
397 v.UpdateLines(v.topline, v.topline+v.height)
400 // Rehighlight the entire buffer
401 v.UpdateLines(v.topline, v.topline+v.height)
406 // Rehighlight the entire buffer
407 v.UpdateLines(v.topline, v.topline+v.height)
417 // Insert a character
418 if v.cursor.HasSelection() {
419 v.cursor.DeleteSelection()
420 v.cursor.ResetSelection()
422 v.eh.Insert(v.cursor.loc, string(e.Rune()))
424 v.UpdateLines(v.cursor.y, v.cursor.y)
426 case *tcell.EventMouse:
430 // Position always seems to be off by one
434 button := e.Buttons()
439 v.MoveToMouseClick(x, y)
442 v.cursor.selectionStart = v.cursor.loc
443 v.cursor.selectionStartX = v.cursor.x
444 v.cursor.selectionStartY = v.cursor.y
446 v.cursor.selectionEnd = v.cursor.loc
447 v.mouseReleased = false
448 case tcell.ButtonNone:
449 // Mouse event with no click
450 if !v.mouseReleased {
451 // Mouse was just released
453 // Relocating here isn't really necessary because the cursor will
454 // be in the right place from the last mouse event
455 // However, if we are running in a terminal that doesn't support mouse motion
456 // events, this still allows the user to make selections, except only after they
458 v.MoveToMouseClick(x, y)
459 v.cursor.selectionEnd = v.cursor.loc
460 v.mouseReleased = true
462 // We don't want to relocate because otherwise the view will be relocated
463 // everytime the user moves the cursor
466 // Scroll up two lines
468 // We don't want to relocate if the user is scrolling
470 case tcell.WheelDown:
471 // Scroll down two lines
473 // We don't want to relocate if the user is scrolling
482 // v.matches = Match(v.buf.rules, v.buf, v)
485 // DisplayView renders the view to the screen
486 func (v *View) DisplayView() {
487 matches := make(SyntaxMatches)
489 // The character number of the character in the top left of the screen
490 charNum := v.cursor.loc + v.cursor.Distance(0, v.topline)
492 // Convert the length of buffer to a string, and get the length of the string
493 // We are going to have to offset by that amount
494 maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
495 // + 1 for the little space after the line number
496 v.lineNumOffset = maxLineLength + 1
498 var highlightStyle tcell.Style
500 for lineN := 0; lineN < v.height; lineN++ {
502 // If the buffer is smaller than the view height
503 // and we went too far, break
504 if lineN+v.topline >= len(v.buf.lines) {
507 line := v.buf.lines[lineN+v.topline]
509 // Write the line number
510 lineNumStyle := tcell.StyleDefault
511 if style, ok := colorscheme["line-number"]; ok {
514 // Write the spaces before the line number if necessary
515 lineNum := strconv.Itoa(lineN + v.topline + 1)
516 for i := 0; i < maxLineLength-len(lineNum); i++ {
517 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
520 // Write the actual line number
521 for _, ch := range lineNum {
522 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
525 // Write the extra space
526 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
531 for _, ch := range line {
532 var lineStyle tcell.Style
533 // Does the current character need to be syntax highlighted?
534 if st, ok := v.matches[charNum]; ok {
536 } else if st, ok := v.lastMatches[charNum]; ok {
539 highlightStyle = tcell.StyleDefault
541 matches[charNum] = highlightStyle
543 if v.cursor.HasSelection() &&
544 (charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd ||
545 charNum <= v.cursor.selectionStart && charNum >= v.cursor.selectionEnd) {
547 lineStyle = tcell.StyleDefault.Reverse(true)
549 if style, ok := colorscheme["selection"]; ok {
553 lineStyle = highlightStyle
557 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
558 for i := 0; i < tabSize-1; i++ {
560 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
563 screen.SetContent(x+tabchars, lineN, ch, nil, lineStyle)
568 // Here we are at a newline
570 // The newline may be selected, in which case we should draw the selection style
571 // with a space to represent it
572 if v.cursor.HasSelection() &&
573 (charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd ||
574 charNum <= v.cursor.selectionStart && charNum >= v.cursor.selectionEnd) {
576 selectStyle := tcell.StyleDefault.Reverse(true)
578 if style, ok := colorscheme["selection"]; ok {
581 screen.SetContent(x+tabchars, lineN, ' ', nil, selectStyle)
588 v.lastMatches = matches
591 // Display renders the view, the cursor, and statusline
592 func (v *View) Display() {